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

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 →

Related Articles