Ruby on Rails has powered countless e-commerce platforms, fintech startups, and SaaS products. Many of these applications need to display prices in multiple currencies or convert between them at checkout. Rather than maintaining stale rate tables, you can integrate an exchange rate API in Ruby to get live, accurate data with minimal code. This guide covers everything from raw Net::HTTP calls to a full Rails service object with background refresh.
Why Ruby Developers Need an Exchange Rate API
Hardcoded exchange rates rot the moment you deploy. Markets move, and your users notice when your prices are off by several percentage points. An exchange rate API in Ruby gives you access to 160+ currencies through a single HTTP call. The Exchange Rate API makes this especially smooth: it uses bearer token authentication, returns clean JSON, and includes a free tier with 1,500 requests per month.
Prerequisites
- Ruby 3.1 or later
- Rails 7.x (for the Rails-specific sections)
- An API key from exchange-rateapi.com
- Bundler installed
Fetching Rates with Net::HTTP
Ruby's standard library includes everything you need. No gems required for a basic integration.
require 'net/http'
require 'json'
require 'uri'
api_key = 'YOUR_API_KEY'
base = 'USD'
uri = URI("https://api.allratestoday.com/v1/latest?base=#{base}")
request = Net::HTTP::Get.new(uri)
request['Authorization'] = "Bearer #{api_key}"
request['Accept'] = 'application/json'
response = Net::HTTP.start(uri.hostname, uri.port, use_ssl: true) do |http|
http.request(request)
end
if response.code == '200'
data = JSON.parse(response.body)
rates = data['rates']
puts "1 USD = #{rates['EUR']} EUR"
puts "1 USD = #{rates['GBP']} GBP"
puts "1 USD = #{rates['JPY']} JPY"
else
puts "Error: #{response.code} - #{response.body}"
end
This works in any Ruby script, Rake task, or IRB session. For production Rails applications, you will want something more structured.
Using Faraday for a Cleaner HTTP Client
Faraday is the most popular HTTP client gem in the Ruby ecosystem. It supports middleware for retries, logging, and error handling.
bundle add faraday
require 'faraday'
require 'json'
client = Faraday.new(
url: 'https://api.allratestoday.com/v1',
headers: {
'Authorization' => 'Bearer YOUR_API_KEY',
'Accept' => 'application/json'
}
) do |conn|
conn.request :retry, max: 2, interval: 0.5
conn.response :raise_error
end
# Fetch latest rates
response = client.get('latest', { base: 'EUR' })
data = JSON.parse(response.body)
puts "Base: #{data['base']}"
data['rates'].each do |currency, rate|
puts " #{currency}: #{rate}"
end
# Convert currency
convert_response = client.get('convert', {
from: 'USD',
to: 'EUR',
amount: 250
})
result = JSON.parse(convert_response.body)
puts "250 USD = #{result['result']} EUR"
The retry middleware is particularly valuable. Network hiccups happen, and an automatic retry saves you from transient failures.
Building a Rails Service Object
Rails applications should encapsulate API logic in a service object. This keeps controllers thin and makes the code testable.
Step 1: Configuration
Add credentials to Rails encrypted credentials or use environment variables:
# .env (using dotenv gem) or set in your deployment platform
EXCHANGE_RATE_API_KEY=your_api_key_here
Create a config initializer at config/initializers/exchange_rate.rb:
Rails.application.config.exchange_rate = ActiveSupport::OrderedOptions.new
Rails.application.config.exchange_rate.api_key = ENV.fetch('EXCHANGE_RATE_API_KEY')
Rails.application.config.exchange_rate.base_url = 'https://api.allratestoday.com/v1'
Rails.application.config.exchange_rate.cache_ttl = 1.hour
Step 2: The Service Object
# app/services/exchange_rate_service.rb
class ExchangeRateService
BASE_URL = 'https://api.allratestoday.com/v1'.freeze
class ApiError < StandardError; end
def initialize(api_key: nil)
@api_key = api_key || Rails.application.config.exchange_rate.api_key
@cache_ttl = Rails.application.config.exchange_rate.cache_ttl
end
# Fetch latest rates with caching
def latest_rates(base: 'USD')
cache_key = "exchange_rates/latest/#{base}"
Rails.cache.fetch(cache_key, expires_in: @cache_ttl) do
response = connection.get('latest', { base: base })
parsed = JSON.parse(response.body)
parsed['rates']
end
end
# Convert an amount between currencies
def convert(from:, to:, amount:)
response = connection.get('convert', {
from: from,
to: to,
amount: amount
})
JSON.parse(response.body)
end
# Fetch historical rates for a specific date
def historical_rates(date:, base: 'USD')
cache_key = "exchange_rates/historical/#{base}/#{date}"
Rails.cache.fetch(cache_key, expires_in: 1.week) do
response = connection.get('historical', {
date: date,
base: base
})
parsed = JSON.parse(response.body)
parsed['rates']
end
end
# Fetch time series data
def time_series(start_date:, end_date:, base: 'USD')
response = connection.get('timeseries', {
start: start_date,
end: end_date,
base: base
})
JSON.parse(response.body)
end
private
def connection
@connection ||= Faraday.new(url: BASE_URL) do |conn|
conn.headers['Authorization'] = "Bearer #{@api_key}"
conn.headers['Accept'] = 'application/json'
conn.request :retry, max: 2, interval: 0.5
conn.response :raise_error
conn.adapter Faraday.default_adapter
end
rescue Faraday::Error => e
raise ApiError, "Exchange rate API error: #{e.message}"
end
end
Step 3: Controller
# app/controllers/currencies_controller.rb
class CurrenciesController < ApplicationController
before_action :set_service
def index
@base = params[:base] || 'USD'
@rates = @service.latest_rates(base: @base)
@popular = @rates.slice('EUR', 'GBP', 'JPY', 'CAD', 'AUD', 'CHF')
end
def convert
result = @service.convert(
from: params[:from],
to: params[:to],
amount: params[:amount].to_f
)
render json: result
end
def historical
@date = params[:date]
@base = params[:base] || 'USD'
@rates = @service.historical_rates(date: @date, base: @base)
end
private
def set_service
@service = ExchangeRateService.new
end
end
Step 4: Routes
# config/routes.rb
Rails.application.routes.draw do
resources :currencies, only: [:index] do
collection do
get :convert
get :historical
end
end
end
Background Rate Refresh with ActiveJob
Rather than waiting for a user request to trigger a cache miss, pre-warm the cache with a background job:
# app/jobs/refresh_exchange_rates_job.rb
class RefreshExchangeRatesJob < ApplicationJob
queue_as :default
BASE_CURRENCIES = %w[USD EUR GBP].freeze
def perform
service = ExchangeRateService.new
BASE_CURRENCIES.each do |base|
Rails.cache.delete("exchange_rates/latest/#{base}")
service.latest_rates(base: base)
Rails.logger.info("Refreshed exchange rates for #{base}")
rescue ExchangeRateService::ApiError => e
Rails.logger.error("Failed to refresh #{base} rates: #{e.message}")
end
end
end
Schedule it with solid_queue, sidekiq-cron, or the whenever gem:
# config/recurring.yml (for Solid Queue in Rails 8)
refresh_rates:
class: RefreshExchangeRatesJob
schedule: every hour
Or with the whenever gem:
# config/schedule.rb
every 1.hour do
runner "RefreshExchangeRatesJob.perform_later"
end
View Helper for Currency Display
Create a helper to format converted amounts in your views:
# app/helpers/currency_helper.rb
module CurrencyHelper
CURRENCY_SYMBOLS = {
'USD' => '$', 'EUR' => "€", 'GBP' => "£",
'JPY' => "¥", 'CAD' => 'C$', 'AUD' => 'A$'
}.freeze
def format_currency(amount, currency_code)
symbol = CURRENCY_SYMBOLS.fetch(currency_code, currency_code)
precision = currency_code == 'JPY' ? 0 : 2
"#{symbol}#{number_with_precision(amount, precision: precision, delimiter: ',')}"
end
def display_rate(from, to, rate)
"1 #{from} = #{number_with_precision(rate, precision: 4)} #{to}"
end
end
Use it in your ERB templates:
<%# app/views/currencies/index.html.erb %>
<h1>Exchange Rates (<%= @base %>)</h1>
<table>
<thead>
<tr>
<th>Currency</th>
<th>Rate</th>
<th>Equivalent of 100 <%= @base %></th>
</tr>
</thead>
<tbody>
<% @popular.each do |currency, rate| %>
<tr>
<td><%= currency %></td>
<td><%= number_with_precision(rate, precision: 4) %></td>
<td><%= format_currency(100 * rate, currency) %></td>
</tr>
<% end %>
</tbody>
</table>
Error Handling and Resilience
Production applications need graceful degradation when the API is unreachable:
def latest_rates_with_fallback(base: 'USD')
latest_rates(base: base)
rescue ApiError => e
Rails.logger.warn("API failed, using stale cache: #{e.message}")
# Try to read expired cache entry
stale = Rails.cache.read("exchange_rates/latest/#{base}")
return stale if stale
raise e # No fallback available
end
Testing with WebMock
Use WebMock to stub API calls in your test suite:
# test/services/exchange_rate_service_test.rb
require 'test_helper'
require 'webmock/minitest'
class ExchangeRateServiceTest < ActiveSupport::TestCase
setup do
@service = ExchangeRateService.new(api_key: 'test_key')
stub_request(:get, /allratestoday\.com\/v1\/latest/)
.to_return(
status: 200,
body: {
base: 'USD',
date: '2026-05-21',
rates: { 'EUR' => 0.92, 'GBP' => 0.79 }
}.to_json,
headers: { 'Content-Type' => 'application/json' }
)
end
test 'fetches latest rates' do
rates = @service.latest_rates(base: 'USD')
assert_equal 0.92, rates['EUR']
assert_equal 0.79, rates['GBP']
end
test 'caches rates' do
@service.latest_rates(base: 'USD')
@service.latest_rates(base: 'USD')
assert_requested :get, /latest/, times: 1
end
end
Conclusion
Integrating an exchange rate API in Ruby follows the patterns that Rails developers already know: service objects for business logic, ActiveJob for background processing, and Rails cache for performance. The clean JSON responses from the API map naturally to Ruby hashes, and Faraday provides the retry logic and middleware that production applications require.
The Exchange Rate API offers 160+ currencies and a free tier of 1,500 requests per month. Combined with caching, that allowance supports even busy Rails applications comfortably.
Ready to add live exchange rates to your Ruby application? Sign up for a free API key at exchange-rateapi.com and check the API documentation for complete endpoint details.
Start Using the Exchange Rate API Today
Free tier with 1,500 requests/month. 160+ currencies updated every 60 seconds. No credit card required.
Get Your Free API Key →