Currency conversion is a core requirement in e-commerce platforms, SaaS billing systems, and financial dashboards. PHP powers a huge portion of the web, and integrating an exchange rate API in PHP is straightforward whether you are working with vanilla PHP or a framework like Laravel. This guide walks you through every step, from raw cURL calls to a production-ready Laravel service with Redis caching.

Why Use an Exchange Rate API in PHP?

Hardcoding exchange rates is a maintenance nightmare. Rates shift constantly, and outdated values lead to pricing errors that cost real money. An exchange rate API in PHP gives you live, accurate data with minimal effort. The Exchange Rate API provides 160+ currencies, a generous free tier of 1,500 requests per month, and clean JSON responses that are trivial to decode in PHP.

Prerequisites

Fetching Rates with Vanilla PHP and cURL

The simplest way to call an exchange rate API in PHP is with cURL, which ships with nearly every PHP installation.


    <?php
    
    $apiKey = 'YOUR_API_KEY';
    $base = 'USD';
    
    $ch = curl_init();
    curl_setopt_array($ch, [
        CURLOPT_URL            => "https://api.allratestoday.com/v1/latest?base={$base}",
        CURLOPT_RETURNTRANSFER => true,
        CURLOPT_HTTPHEADER     => [
            "Authorization: Bearer {$apiKey}",
            "Accept: application/json",
        ],
    ]);
    
    $response = curl_exec($ch);
    $httpCode = curl_getinfo($ch, CURLINFO_HTTP_CODE);
    curl_close($ch);
    
    if ($httpCode !== 200) {
        die("API request failed with status {$httpCode}");
    }
    
    $data = json_decode($response, true);
    
    echo "1 USD = " . $data['rates']['EUR'] . " EUR\n";
    echo "1 USD = " . $data['rates']['GBP'] . " GBP\n";
    

This gives you a working integration in under 20 lines. The /v1/latest endpoint returns all available rates for the given base currency.

Using Guzzle for Cleaner HTTP

Most modern PHP projects already pull in Guzzle via Composer. It provides a cleaner, object-oriented interface and built-in error handling.


    composer require guzzlehttp/guzzle
    

    <?php
    
    require 'vendor/autoload.php';
    
    use GuzzleHttp\Client;
    use GuzzleHttp\Exception\RequestException;
    
    $client = new Client([
        'base_uri' => 'https://api.allratestoday.com/v1/',
        'headers'  => [
            'Authorization' => 'Bearer YOUR_API_KEY',
            'Accept'        => 'application/json',
        ],
        'timeout' => 10,
    ]);
    
    try {
        // Fetch latest rates
        $response = $client->get('latest', [
            'query' => ['base' => 'USD'],
        ]);
    
        $data = json_decode($response->getBody(), true);
        $rates = $data['rates'];
    
        echo "EUR: {$rates['EUR']}\n";
        echo "JPY: {$rates['JPY']}\n";
    
        // Direct currency conversion
        $convertResponse = $client->get('convert', [
            'query' => [
                'from'   => 'USD',
                'to'     => 'EUR',
                'amount' => 250,
            ],
        ]);
    
        $conversion = json_decode($convertResponse->getBody(), true);
        echo "250 USD = {$conversion['result']} EUR\n";
    
    } catch (RequestException $e) {
        echo "Error: " . $e->getMessage() . "\n";
    }
    

Guzzle also makes it simple to add retry middleware, logging, and connection pooling when your application scales.

Building a Laravel Service Class

Laravel applications benefit from wrapping API calls inside a dedicated service class. This keeps controllers thin and makes the logic easy to test.

Step 1: Configuration

Add your API key to .env:


    EXCHANGE_RATE_API_KEY=your_api_key_here
    EXCHANGE_RATE_BASE_URL=https://api.allratestoday.com/v1
    

Publish a config file at config/exchange-rate.php:


    <?php
    
    return [
        'api_key'  => env('EXCHANGE_RATE_API_KEY'),
        'base_url' => env('EXCHANGE_RATE_BASE_URL', 'https://api.allratestoday.com/v1'),
        'cache_ttl' => 3600, // Cache rates for 1 hour
    ];
    

Step 2: The Service Class


    <?php
    
    namespace App\Services;
    
    use Illuminate\Support\Facades\Cache;
    use Illuminate\Support\Facades\Http;
    use Illuminate\Http\Client\RequestException;
    
    class ExchangeRateService
    {
        private string $baseUrl;
        private string $apiKey;
        private int $cacheTtl;
    
        public function __construct()
        {
            $this->baseUrl  = config('exchange-rate.base_url');
            $this->apiKey   = config('exchange-rate.api_key');
            $this->cacheTtl = config('exchange-rate.cache_ttl', 3600);
        }
    
        /**
         * Get latest exchange rates for a base currency.
         */
        public function getLatestRates(string $base = 'USD'): array
        {
            $cacheKey = "exchange_rates_{$base}";
    
            return Cache::remember($cacheKey, $this->cacheTtl, function () use ($base) {
                $response = Http::withToken($this->apiKey)
                    ->acceptJson()
                    ->get("{$this->baseUrl}/latest", ['base' => $base]);
    
                $response->throw();
    
                return $response->json('rates');
            });
        }
    
        /**
         * Convert an amount between two currencies.
         */
        public function convert(string $from, string $to, float $amount): array
        {
            $response = Http::withToken($this->apiKey)
                ->acceptJson()
                ->get("{$this->baseUrl}/convert", [
                    'from'   => $from,
                    'to'     => $to,
                    'amount' => $amount,
                ]);
    
            $response->throw();
    
            return $response->json();
        }
    
        /**
         * Fetch historical rates for a specific date.
         */
        public function getHistoricalRates(string $date, string $base = 'USD'): array
        {
            $cacheKey = "exchange_rates_historical_{$base}_{$date}";
    
            return Cache::remember($cacheKey, now()->addDay(), function () use ($date, $base) {
                $response = Http::withToken($this->apiKey)
                    ->acceptJson()
                    ->get("{$this->baseUrl}/historical", [
                        'date' => $date,
                        'base' => $base,
                    ]);
    
                $response->throw();
    
                return $response->json('rates');
            });
        }
    }
    

Register the service in AppServiceProvider if you want to bind it as a singleton:


    $this->app->singleton(ExchangeRateService::class);
    

Step 3: Controller


    <?php
    
    namespace App\Http\Controllers;
    
    use App\Services\ExchangeRateService;
    use Illuminate\Http\Request;
    
    class CurrencyController extends Controller
    {
        public function __construct(private ExchangeRateService $rates) {}
    
        public function index()
        {
            $rates = $this->rates->getLatestRates('USD');
            $currencies = ['EUR', 'GBP', 'JPY', 'CAD', 'AUD'];
    
            return view('currency.index', [
                'rates'      => array_intersect_key($rates, array_flip($currencies)),
                'base'       => 'USD',
            ]);
        }
    
        public function convert(Request $request)
        {
            $validated = $request->validate([
                'from'   => 'required|string|size:3',
                'to'     => 'required|string|size:3',
                'amount' => 'required|numeric|min:0.01',
            ]);
    
            $result = $this->rates->convert(
                $validated['from'],
                $validated['to'],
                $validated['amount']
            );
    
            return view('currency.result', compact('result'));
        }
    }
    

Step 4: Blade Template


    {{-- resources/views/currency/index.blade.php --}}
    @extends('layouts.app')
    
    @section('content')
    <div class="max-w-2xl mx-auto py-8">
        <h1 class="text-2xl font-bold mb-6">Live Exchange Rates ({{ $base }})</h1>
    
        <table class="w-full border-collapse">
            <thead>
                <tr class="bg-gray-100">
                    <th class="p-3 text-left">Currency</th>
                    <th class="p-3 text-right">Rate</th>
                </tr>
            </thead>
            <tbody>
                @foreach ($rates as $currency => $rate)
                <tr class="border-b">
                    <td class="p-3">{{ $currency }}</td>
                    <td class="p-3 text-right font-mono">{{ number_format($rate, 4) }}</td>
                </tr>
                @endforeach
            </tbody>
        </table>
    
        <h2 class="text-xl font-bold mt-10 mb-4">Convert Currency</h2>
    
        <form method="POST" action="{{ route('currency.convert') }}" class="space-y-4">
            @csrf
            <div class="flex gap-4">
                <input type="number" name="amount" step="0.01" min="0.01"
                       placeholder="Amount" class="border p-2 rounded w-32" required>
                <input type="text" name="from" maxlength="3"
                       placeholder="From (e.g. USD)" class="border p-2 rounded w-32" required>
                <input type="text" name="to" maxlength="3"
                       placeholder="To (e.g. EUR)" class="border p-2 rounded w-32" required>
            </div>
            <button type="submit" class="bg-blue-600 text-white px-6 py-2 rounded">
                Convert
            </button>
        </form>
    </div>
    @endsection
    

Caching with Redis

The service class above already uses Laravel's cache facade, which works with Redis out of the box once you configure it in config/cache.php. This is critical in production: caching exchange rates slashes API usage and speeds up responses.

For vanilla PHP projects, you can use predis or phpredis directly:


    <?php
    
    $redis = new Redis();
    $redis->connect('127.0.0.1', 6379);
    
    $cacheKey = 'exchange_rates_usd';
    $cached = $redis->get($cacheKey);
    
    if ($cached) {
        $rates = json_decode($cached, true);
    } else {
        // Fetch from API (using the cURL example above)
        $rates = fetchLatestRates('USD');
        $redis->setex($cacheKey, 3600, json_encode($rates));
    }
    

Caching for one hour is a sensible default. Historical rates never change, so those can be cached indefinitely.

Handling Errors Gracefully

Production code needs to handle network failures, rate limits, and invalid responses. Here is a resilient wrapper:


    public function safeConvert(string $from, string $to, float $amount): ?array
    {
        try {
            return $this->convert($from, $to, $amount);
        } catch (RequestException $e) {
            Log::error('Exchange rate API error', [
                'status'  => $e->response?->status(),
                'message' => $e->getMessage(),
            ]);
    
            if ($e->response?->status() === 429) {
                // Rate limited — fall back to cached rates
                $rate = Cache::get("exchange_rates_{$from}");
                if ($rate && isset($rate[$to])) {
                    return [
                        'result' => round($amount * $rate[$to], 2),
                        'cached' => true,
                    ];
                }
            }
    
            return null;
        }
    }
    

Scheduling Rate Refresh with Artisan

For applications that display rates on a dashboard, you can pre-warm the cache with a scheduled command:


    // app/Console/Commands/RefreshExchangeRates.php
    Artisan::command('rates:refresh', function () {
        $service = app(ExchangeRateService::class);
        $bases = ['USD', 'EUR', 'GBP'];
    
        foreach ($bases as $base) {
            Cache::forget("exchange_rates_{$base}");
            $service->getLatestRates($base);
            $this->info("Refreshed rates for {$base}");
        }
    });
    
    // In routes/console.php or Kernel schedule
    Schedule::command('rates:refresh')->hourly();
    

Testing the Integration

Use Laravel's HTTP fake to write tests without hitting the real API:


    public function test_it_fetches_latest_rates(): void
    {
        Http::fake([
            '*/latest*' => Http::response([
                'base'  => 'USD',
                'rates' => ['EUR' => 0.92, 'GBP' => 0.79],
            ]),
        ]);
    
        $service = new ExchangeRateService();
        $rates = $service->getLatestRates('USD');
    
        $this->assertEquals(0.92, $rates['EUR']);
        $this->assertEquals(0.79, $rates['GBP']);
    }
    

Conclusion

Integrating an exchange rate API in PHP takes minutes with cURL and barely longer with a full Laravel service layer. By combining the Exchange Rate API with caching, error handling, and scheduled refreshes, you get a robust currency system that scales with your application.

The free tier at Exchange Rate API gives you 1,500 requests per month and access to 160+ currencies. With the caching strategies shown above, that budget goes a long way even for high-traffic applications.

Ready to add live exchange rates to your PHP project? Sign up for a free API key at exchange-rateapi.com and start converting currencies in minutes. Check out the full API documentation for every endpoint and parameter available.

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