Skip to content
Ruby guide

Exchange rate API in Ruby and Rails

Fetch live and historical currency rates with Net::HTTP or Faraday, convert amounts with BigDecimal, and cache everything in Rails.cache with a stale fallback.

Last updated: · fxapi team

This guide shows how to use an exchange rate API in Ruby: a dependency-free Net::HTTP client with full error handling, the same calls with Faraday, and a Rails service that caches rates in Rails.cache and converts amounts with BigDecimal. You’ll fetch latest rates, look up a historical date and convert currencies.

Setup

  1. Get a key from the free sign-up.
  2. Store it as FXAPI_KEY in your environment or Rails credentials.

Requests are GET https://api.fxapi.com/v1/<endpoint> with the key in the apikey header. The endpoints used here are latest, historical and convert.

Exchange rate API in Ruby with Net::HTTP

require "net/http"
require "json"
require "uri"

module FxApi
  BASE = "https://api.fxapi.com/v1"

  class Error < StandardError
    attr_reader :status, :errors

    def initialize(status, message, errors = nil)
      super("#{status}: #{message}")
      @status = status
      @errors = errors
    end
  end

  def self.get(path, **params)
    uri = URI("#{BASE}#{path}")
    uri.query = URI.encode_www_form(params)
    req = Net::HTTP::Get.new(uri)
    req["apikey"] = ENV.fetch("FXAPI_KEY")

    res = Net::HTTP.start(uri.host, uri.port, use_ssl: true, open_timeout: 5, read_timeout: 10) do |http|
      http.request(req)
    end
    body = begin
      JSON.parse(res.body)
    rescue JSON::ParserError
      {}
    end

    case res.code.to_i
    when 200 then body
    when 401 then raise Error.new(401, "Invalid API key")
    when 403 then raise Error.new(403, "Endpoint or option not included in your plan")
    when 422 then raise Error.new(422, "Validation error", body["errors"])
    when 429
      raise Error.new(429, "Rate limit reached, monthly quota left: #{res['X-RateLimit-Remaining-Quota-Month']}")
    else raise Error.new(res.code.to_i, res.message)
    end
  end
end

latest = FxApi.get("/latest", base_currency: "EUR", currencies: "USD,GBP,CHF")
puts latest.dig("meta", "last_updated_at")
puts latest.dig("data", "USD", "value")

hist = FxApi.get("/historical", date: "2024-12-31", base_currency: "USD", currencies: "EUR")
puts hist.dig("data", "EUR", "value")

Status codes to plan for:

StatusMeaningWhat to do
401Invalid API keyCheck the key; don’t retry
403Endpoint or option not in your planUpgrade or use another endpoint
422Validation error, errors keyed by parameterFix the input; doesn’t count against quota
429Monthly quota used up, or Free plan’s 10 requests/minuteServe cached rates; retry later

Historical values are end-of-day rates in UTC; date can be any day from 1999-01-01 up to yesterday. For whole series, see the historical exchange rates API.

Currency conversion with BigDecimal

On Basic and higher plans, /v1/convert returns converted amounts:

conv = FxApi.get("/convert", value: 250, base_currency: "USD", currencies: "EUR")
puts conv.dig("data", "EUR", "value")

On the Free plan, multiply by the latest rate. Use BigDecimal, never Float, for money:

require "bigdecimal"

rate = BigDecimal(FxApi.get("/latest", base_currency: "USD", currencies: "EUR").dig("data", "EUR", "value").to_s)
eur = (BigDecimal("250.00") * rate).round(2, :banker)

bigdecimal is a bundled gem since Ruby 3.4 – add it to your Gemfile if you’re not on Rails. Different currencies need different decimal places (JPY 0, KWD 3); the rounding guide shows how to read them from /v1/currencies.

Using Faraday

require "faraday"

FX = Faraday.new(
  url: "https://api.fxapi.com/v1/",
  headers: { "apikey" => ENV.fetch("FXAPI_KEY") },
  request: { timeout: 10, open_timeout: 5 }
) do |f|
  f.response :raise_error # raises Faraday::ClientError / ServerError on 4xx / 5xx
  f.response :json        # parses JSON bodies (runs first on the response)
end

begin
  res = FX.get("latest", base_currency: "USD") # all 190+ currencies in one call
  rates = res.body["data"].transform_values { |r| r["value"] }
rescue Faraday::ClientError => e
  status = e.response[:status]
  if status == 422
    Rails.logger.error("fxapi validation: #{e.response[:body]['errors']}")
  elsif status == 429
    # quota or per-minute limit reached: fall back to cached rates
  end
  raise
end

Faraday 2 includes the JSON response middleware. Middleware listed later sits closer to the HTTP adapter, so :json parses the body before :raise_error inspects it.

Rails: caching exchange rates with Rails.cache

# app/services/exchange_rates.rb
class ExchangeRates
  TTL = 1.hour # Basic plan updates hourly; use 1.minute on Professional, 1.day on Free

  def self.latest(base = "USD")
    key = "fxapi/latest/#{base}"
    Rails.cache.fetch(key, expires_in: TTL, race_condition_ttl: 30.seconds) do
      FxApi.get("/latest", base_currency: base).tap do |data|
        Rails.cache.write("#{key}/last-good", data)
      end
    end
  rescue FxApi::Error, Net::OpenTimeout, Net::ReadTimeout => e
    Rails.logger.warn("fxapi: #{e.message}")
    Rails.cache.read("#{key}/last-good") || raise
  end

  def self.historical(date, base = "USD")
    Rails.cache.fetch("fxapi/historical/#{base}/#{date}") do # past rates never change
      FxApi.get("/historical", date: date.to_s, base_currency: base)
    end
  end

  def self.convert(amount, from:, to:, digits: 2)
    rate = BigDecimal(latest(from).dig("data", to, "value").to_s)
    (BigDecimal(amount.to_s) * rate).round(digits, :banker)
  end
end

ExchangeRates.convert("49.00", from: "USD", to: "EUR")

race_condition_ttl stops many requests from refreshing the same expired key at once, and the last-good copy keeps checkout working if fxapi is unreachable or you hit a 429. Use a shared store such as Redis or Solid Cache so all Puma workers and servers share one cached response. Store meta.last_updated_at with each order or invoice so you can always show which rate was applied – more in caching exchange rates.

Testing without spending quota

Stub HTTP calls in your test suite so specs are fast and deterministic. With WebMock:

stub_request(:get, %r{api\.fxapi\.com/v1/latest})
  .to_return(
    status: 200,
    headers: { "Content-Type" => "application/json" },
    body: { meta: { last_updated_at: "2026-10-01T23:59:59Z" },
            data: { "EUR" => { code: "EUR", value: 0.9 } } }.to_json
  )

expect(ExchangeRates.convert("10.00", from: "USD", to: "EUR")).to eq(BigDecimal("9.00"))

Return status: 429 to verify that the stale fallback kicks in. For end-to-end checks against the live API, the Professional and Enterprise plans include sandbox keys that return random dummy values with X-Cost: 0, so CI runs don’t use your quota.

Using the official Ruby gem

The fxapi gem exposes one method per endpoint with positional arguments and returns the raw RestClient response:

gem install fxapi
require "fxapi"
require "json"

fx = Fxapi::Endpoints.new(apikey: ENV.fetch("FXAPI_KEY"))

latest = JSON.parse(fx.latest("EUR", "USD,GBP").body)
hist = JSON.parse(fx.historical("2024-12-31", "USD", "EUR").body)

The gem sends the key as a query parameter rather than a header, so it can show up in proxy logs. If that matters for you, use the Net::HTTP or Faraday client above.

Next steps

The examples in this guide use fxapi, a foreign exchange rate API with a free plan of 300 requests a month – no credit card required.

Frequently asked questions

Should I use Net::HTTP or Faraday for the exchange rate API?
Net::HTTP ships with Ruby and is fine for scripts and small services. Faraday adds middleware for JSON parsing, error raising and retries, which keeps Rails code shorter.
How do I avoid floating point errors when converting currencies in Ruby?
Parse the JSON with decimal_class: BigDecimal or wrap rates in BigDecimal(value.to_s), multiply, then round once to the currency’s decimal places. See currency rounding and precision.
How long should Rails cache exchange rates?
Use your plan’s update interval as expires_in: one day on Free, one hour on Basic, 60 seconds on Professional and Enterprise. Historical rates can be cached without expiry.
Can I convert currencies on the Free plan?
Yes, by multiplying with a rate from /v1/latest. The /v1/convert endpoint, which returns converted amounts directly, requires the Basic plan or higher.
Is there an official Ruby gem?
Yes, fxapi on RubyGems. It returns the raw HTTP response, so you parse the JSON yourself.
Free plan · no credit card

Get your free exchange rate API key

300 requests a month, latest and historical rates, fluctuation and averages – free forever. Upgrade when you need faster updates or more requests.