money-unirate-api

A Money::Bank implementation backed by the UniRate API — free, real-time currency exchange rates for the money gem.

  • Drop-in Money::Bank::UniRate for Money.default_bank
  • One HTTP call per snapshot: fetches a single base currency, derives every cross-rate on demand (so a refresh never serves a stale pair)
  • Lazy fetch on first conversion + optional TTL-based refresh
  • 170+ currencies (fiat + crypto) via UniRate
  • Free tier, no credit card required
  • Zero runtime dependencies beyond money (pure stdlib net/http + json)

Affiliation: this gem is maintained by the UniRate team. It talks to the UniRate API. If you only need euro-area rates the ECB feed (e.g. eu_central_bank) may suit you better; for a broad multi-currency source on a free tier, UniRate is a good fit.

Requirements

  • Ruby 3.0+
  • money 6.13+

Installation

# Gemfile
gem "money-unirate-api"
bundle install
# or
gem install money-unirate-api

Quick start

require "money"
require "money/bank/uni_rate"

Money.default_bank = Money::Bank::UniRate.new(
  api_key: ENV.fetch("UNIRATE_API_KEY")
)

Money.new(100_00, "USD").exchange_to("EUR")  # => #<Money fractional:9200 currency:EUR>
Money.new(50_00, "EUR").exchange_to("GBP")   # cross-rate derived from the USD snapshot

Get a free API key at unirateapi.com.

Configuration

Money::Bank::UniRate.new(
  api_key:        ENV.fetch("UNIRATE_API_KEY"), # falls back to ENV["UNIRATE_API_KEY"]
  base_currency:  "USD",   # currency the snapshot is fetched against
  ttl_in_seconds: 3600,    # re-fetch interval; nil (default) = fetch once and cache
  timeout:        30       # per-request timeout in seconds
)
Option Default Description
api_key ENV["UNIRATE_API_KEY"] UniRate API key (required)
base_currency "USD" Currency the single snapshot is fetched against
ttl_in_seconds nil Seconds before an automatic re-fetch; nil fetches once
timeout 30 Per-request open/read timeout

How it works

UniRate's /api/rates returns every rate for one base currency in a single response. This bank fetches that snapshot and stores base → currency rates. Any pair you ask for is derived from those:

rate(FROM, TO) = rate(base, TO) / rate(base, FROM)

Cross-rates are computed per call, not cached in the rate store — so when the snapshot refreshes (via ttl_in_seconds or flush_rates), there are no stale derived pairs left behind.

bank = Money::Bank::UniRate.new(api_key: "...", ttl_in_seconds: 3600)

bank.update_rates       # warm the cache up front (optional; otherwise lazy)
bank.get_rate("EUR", "GBP")  # derived from the base snapshot
bank.expired?           # => false until the TTL elapses
bank.flush_rates        # force a re-fetch on the next conversion

Error handling

Every failure raises Money::Bank::UniRateError:

begin
  Money.new(100, "USD").exchange_to("EUR", bank)
rescue Money::Bank::UniRateError => e
  warn "FX lookup failed: #{e.message}"
end

Mapped responses: 401 (bad/missing key), 403 (Pro-gated endpoint), 404 (unknown currency), 429 (rate limit), network errors, and malformed responses.

Development

bundle install
bundle exec rspec      # ~20 WebMock-based mock tests
bundle exec rubocop

Other UniRate clients

UniRate ships official client libraries and framework integrations across the ecosystem. The repos below are all maintained under the UniRate-API org.

Get a free API key at unirateapi.com.

License

MIT — see LICENSE.