Last updated: 26 June 2026
Introduction
If you are building a Laravel application that handles pricing, invoicing, payments, or anything involving multiple currencies, you need a reliable source of live exchange rate data. This tutorial walks through integrating the CurrencyFreaks currency API into a Laravel project from scratch, from installation to a maintainable implementation with caching and scheduled updates.
Quick answer: To use the CurrencyFreaks API in Laravel, store your API key in config/services.php, wrap the calls in a service class built on the Http facade, cache the responses, and expose them through a controller and a Blade view. The free plan covers real-time latest rates for 1024 currencies with 1,000 API calls per month and no credit card.
By the end of this guide you will have:
- A Laravel service class that fetches live rates from the CurrencyFreaks API
- A controller that exposes those rates to your views
- A Blade template that displays them cleanly
- Laravel Cache integration to avoid redundant API calls
- An Artisan command with scheduled execution to store rates in your database
CurrencyFreaks covers 1024 currencies (166 fiat, 4 metals, and 854 cryptocurrencies) with a free plan that includes 1,000 API calls per month and no credit card. The same API key works across all the code in this tutorial.
What You Can Build on the Free Plan vs a Paid Plan
A quick map before you write code, because two features in this tutorial are paid-only and it is better to know up front:
| Feature used in this guide | Free plan | Paid plan |
|---|---|---|
Latest rates (/rates/latest) |
Yes | Yes |
Filter currencies with symbols |
Yes | Yes |
| USD base currency | Yes | Yes |
Custom base currency (the base parameter) |
No | Yes |
Historical rates (/rates/historical) |
No | Yes |
| Higher update frequency and call volume | No | Yes |
Everything in Steps 1 through 8 runs on the free plan. The historical endpoint and custom base currency are flagged clearly where they appear, and both return HTTP 402 if you call them on the free plan.
Prerequisites
- Laravel 10 or 11
- PHP 8.1+
- Composer
- A free CurrencyFreaks API key (sign up here)
Step 1: Install Guzzle HTTP Client
Laravel ships with Guzzle support built in via the HTTP facade, so no separate installation is needed in Laravel 9+. If you are on an older version, install Guzzle directly:
composer require guzzlehttp/guzzle
For Laravel 9 and above, the Illuminate\Support\Facades\Http facade wraps Guzzle and is the recommended approach. All examples in this tutorial use the Http facade.
Step 2: Store Your API Key
Add your CurrencyFreaks API key to your .env file:
CURRENCYFREAKS_API_KEY=YOUR_API_KEY
Then register it in config/services.php:
'currencyfreaks' => [
'key' => env('CURRENCYFREAKS_API_KEY'),
'base_url' => 'https://api.currencyfreaks.com/v2.0',
],
Never hard-code your API key in controller or service files. Using the config system keeps your key out of version control, and it lets you rotate the key without touching application code.
Step 3: Create a CurrencyFreaks Service Class
Rather than calling the API directly from a controller, encapsulate the integration in a dedicated service class. This keeps your controllers thin and makes the service easy to test and reuse.
Create the file at app/Services/CurrencyFreaksService.php:
<?php
namespace App\Services;
use Illuminate\Support\Facades\Http;
use Illuminate\Support\Facades\Cache;
use Illuminate\Support\Facades\Log;
class CurrencyFreaksService
{
protected string $apiKey;
protected string $baseUrl;
public function __construct()
{
$this->apiKey = config('services.currencyfreaks.key');
$this->baseUrl = config('services.currencyfreaks.base_url');
}
/**
* Fetch latest exchange rates, with a 60-minute cache.
*
* Free plan: leave $base as 'USD'. The base parameter is a paid
* feature, so this method only sends it when you request a non-USD base.
*
* @param string $base Base currency (USD on the free plan)
* @param array $symbols Currencies to return, e.g. ['EUR','GBP','JPY']
* @return array|null
*/
public function getLatestRates(string $base = 'USD', array $symbols = []): ?array
{
$cacheKey = 'cf_rates_' . $base . '_' . implode('_', $symbols);
return Cache::remember($cacheKey, now()->addMinutes(60), function () use ($base, $symbols) {
$params = ['apikey' => $this->apiKey];
if (!empty($symbols)) {
$params['symbols'] = implode(',', $symbols);
}
// The base parameter is a paid feature. On the free plan USD is the
// default, so we omit base entirely to avoid an HTTP 402 response.
if (strtoupper($base) !== 'USD') {
$params['base'] = strtoupper($base);
}
$response = Http::timeout(10)->get("{$this->baseUrl}/rates/latest", $params);
if ($response->failed()) {
Log::error('CurrencyFreaks API error', [
'status' => $response->status(),
'body' => $response->body(),
]);
return null;
}
return $response->json();
});
}
/**
* Fetch historical rates for a given date.
*
* PAID PLANS ONLY. The /rates/historical endpoint is not available on the
* free plan and returns HTTP 402 there. Use this only on a paid plan.
*
* @param string $date Format: YYYY-MM-DD
* @param string $base Base currency (paid feature when not USD)
* @param array $symbols Currencies to return
* @return array|null
*/
public function getHistoricalRates(string $date, string $base = 'USD', array $symbols = []): ?array
{
$cacheKey = "cf_historical_{$date}_{$base}_" . implode('_', $symbols);
return Cache::remember($cacheKey, now()->addHours(24), function () use ($date, $base, $symbols) {
$params = [
'apikey' => $this->apiKey,
'date' => $date,
];
if (!empty($symbols)) {
$params['symbols'] = implode(',', $symbols);
}
if (strtoupper($base) !== 'USD') {
$params['base'] = strtoupper($base);
}
$response = Http::timeout(10)->get("{$this->baseUrl}/rates/historical", $params);
if ($response->failed()) {
Log::error('CurrencyFreaks historical API error', [
'date' => $date,
'status' => $response->status(),
'body' => $response->body(),
]);
return null;
}
return $response->json();
});
}
}
A few things worth noting in this service:
Caching: Latest rates are cached for 60 minutes using Laravel's Cache facade. If 1,000 users load your pricing page in an hour, you make one API call instead of 1,000. On the free plan (1,000 calls per month), this matters. Historical rates are cached for 24 hours, since a past date never changes.
The symbols parameter: Passing only the currencies you need (for example ['EUR', 'GBP', 'JPY']) reduces response size and keeps things fast. To get all 1024 currencies, omit the symbols parameter. The symbols filter is available on every plan, including the free one.
The base parameter: Changing the base currency is a paid feature. The service omits base whenever it is USD, so free-plan requests work without triggering an HTTP 402. Pass a non-USD base only on a paid plan.
Error handling: Failed requests are logged and return null rather than throwing exceptions. The CurrencyFreaks API returns error details as a JSON object under error.info, which is captured here in the logged response body. Handle the null case in your controller.
Step 4: Register the Service
Bind the service in app/Providers/AppServiceProvider.php so Laravel's container can inject it:
use App\Services\CurrencyFreaksService;
public function register(): void
{
$this->app->singleton(CurrencyFreaksService::class, function () {
return new CurrencyFreaksService();
});
}
Step 5: Create a Controller
Generate a controller:
php artisan make:controller CurrencyRateController
Then fill it in at app/Http/Controllers/CurrencyRateController.php:
<?php
namespace App\Http\Controllers;
use App\Services\CurrencyFreaksService;
class CurrencyRateController extends Controller
{
public function __construct(
protected CurrencyFreaksService $currencyService
) {}
public function index()
{
// USD base keeps this on the free plan.
$data = $this->currencyService->getLatestRates(
base: 'USD',
symbols: ['EUR', 'GBP', 'JPY', 'CAD', 'AUD', 'CHF', 'SGD', 'INR']
);
if (!$data) {
return view('currency.index', ['rates' => [], 'error' => 'Unable to fetch rates. Please try again.']);
}
return view('currency.index', [
'rates' => $data['rates'] ?? [],
'base' => $data['base'] ?? 'USD',
'updatedAt' => $data['date'] ?? now()->toDateTimeString(),
'error' => null,
]);
}
}
Add the route in routes/web.php:
use App\Http\Controllers\CurrencyRateController;
Route::get('/rates', [CurrencyRateController::class, 'index'])->name('rates.index');
Step 6: Create the Blade Template
Create resources/views/currency/index.blade.php:
@extends('layouts.app')
@section('title', 'Live Exchange Rates')
@section('content')
<div class="container mx-auto px-4 py-8">
<h1 class="text-2xl font-bold mb-2">Live Exchange Rates</h1>
@if($error)
<div class="bg-red-100 text-red-700 p-4 rounded mb-6">
{{ $error }}
</div>
@else
<p class="text-gray-500 text-sm mb-6">
Base currency: <strong>{{ $base }}</strong> |
Last updated: <strong>{{ $updatedAt }}</strong>
</p>
<table class="w-full border-collapse text-left">
<thead>
<tr class="bg-gray-100">
<th class="p-3 border">Currency</th>
<th class="p-3 border">Rate (vs {{ $base }})</th>
</tr>
</thead>
<tbody>
@forelse($rates as $currency => $rate)
<tr class="hover:bg-gray-50">
<td class="p-3 border font-medium">{{ $currency }}</td>
<td class="p-3 border">{{ number_format((float)$rate, 4) }}</td>
</tr>
@empty
<tr>
<td colspan="2" class="p-3 border text-gray-400">No rates available.</td>
</tr>
@endforelse
</tbody>
</table>
@endif
</div>
@endsection
Step 7: Store Daily Rates in the Database via an Artisan Command
For applications that need their own rate history without calling the API on every request, store the latest rates in your database on a schedule. Run this once a day and you build up a clean history over time, all on the free plan, because it uses the latest-rates endpoint rather than the paid historical one.
First, create the migration:
php artisan make:migration create_exchange_rates_table
// database/migrations/xxxx_create_exchange_rates_table.php
public function up(): void
{
Schema::create('exchange_rates', function (Blueprint $table) {
$table->id();
$table->string('base_currency', 3);
$table->string('target_currency', 3);
$table->decimal('rate', 20, 8);
$table->date('rate_date');
$table->timestamps();
$table->unique(['base_currency', 'target_currency', 'rate_date']);
});
}
php artisan migrate
Next, create the Eloquent model the command will write to:
php artisan make:model ExchangeRate
<?php
// app/Models/ExchangeRate.php
namespace App\Models;
use Illuminate\Database\Eloquent\Model;
class ExchangeRate extends Model
{
protected $fillable = [
'base_currency',
'target_currency',
'rate',
'rate_date',
];
}
Now create the Artisan command:
php artisan make:command FetchExchangeRates
<?php
namespace App\Console\Commands;
use App\Services\CurrencyFreaksService;
use App\Models\ExchangeRate;
use Illuminate\Console\Command;
use Carbon\Carbon;
class FetchExchangeRates extends Command
{
protected $signature = 'rates:fetch';
protected $description = 'Fetch the latest exchange rates from CurrencyFreaks and store them in the database';
public function handle(CurrencyFreaksService $service): int
{
$today = Carbon::today()->toDateString();
$this->info("Fetching latest rates for {$today}...");
// Latest rates keep this on the free plan. USD base, no base parameter sent.
$data = $service->getLatestRates('USD', ['EUR', 'GBP', 'JPY', 'CAD', 'AUD', 'SGD', 'INR', 'CHF']);
if (!$data || empty($data['rates'])) {
$this->error('Failed to fetch rates.');
return self::FAILURE;
}
$base = $data['base'] ?? 'USD';
foreach ($data['rates'] as $currency => $rate) {
ExchangeRate::updateOrCreate(
[
'base_currency' => $base,
'target_currency' => $currency,
'rate_date' => $today,
],
['rate' => $rate]
);
}
$this->info('Rates stored successfully: ' . count($data['rates']) . ' currencies.');
return self::SUCCESS;
}
}
Run it manually:
php artisan rates:fetch
Backfilling past dates (paid plans): This command stores rates going forward. If you need historical rates for dates before you started collecting, use the
getHistoricalRates()method shown in Step 3, which calls the/rates/historicalendpoint. That endpoint is available on paid plans only. See pricing for details.
Step 8: Schedule the Command
Register the schedule in app/Console/Kernel.php (Laravel 10) or routes/console.php (Laravel 11):
Laravel 10, in app/Console/Kernel.php:
protected function schedule(Schedule $schedule): void
{
// Store the latest rates once a day to build your own history.
$schedule->command('rates:fetch')->dailyAt('00:05');
}
Laravel 11, in routes/console.php:
use Illuminate\Support\Facades\Schedule;
Schedule::command('rates:fetch')->dailyAt('00:05');
Make sure your server's cron is running the Laravel scheduler:
* * * * * cd /path-to-your-project && php artisan schedule:run >> /dev/null 2>&1
A daily run uses about 30 API calls per month, well within the free plan's 1,000. If you need rates more often, the cache in Step 3 already protects your live pages, so you rarely need to schedule more frequently than hourly.
Handling API Errors in Production
CurrencyFreaks returns HTTP 200 on success. On failure it returns a 4xx code with an error message in the response body under error.info. These are the codes you are most likely to encounter in a Laravel integration, taken from the CurrencyFreaks documentation:
| HTTP Status | Meaning | What to do |
|---|---|---|
| 400 | Missing API key, invalid parameters, or invalid date format | Check that your key is set and your symbols, base, and date values are valid |
| 401 | API key is invalid or inactive | Verify the key in your .env, or check your account status |
| 402 | The feature requires a higher plan (custom base currency or historical rates) | Stay on USD and latest rates, or upgrade your plan |
| 404 | Rates for the requested date or currency are not in the database | Check coverage with the historical-data-limits endpoint, or pick a supported date |
| 429 | You have exceeded your plan's monthly request limit | Increase your cache TTL, reduce calls, or upgrade your plan |
| 206 | Partial response (some requested currencies returned) | Handle the rates that did return, and log the gap |
The single most common surprise on the free plan is 402. It means you asked for a paid feature. In this tutorial that happens only if you send a non-USD base or call the historical endpoint, both of which are clearly marked above. The service class already logs failed responses. For production, add a fallback: if a live call fails and the cache is empty, serve the last known rates from your exchange_rates table rather than showing an error to users.
Using the Same API in WordPress or WooCommerce
Running a WordPress or WooCommerce store alongside your Laravel app? The same CurrencyFreaks API works there using wp_remote_get(), with the same endpoint, key, and response structure. Only the HTTP library differs. See the dedicated WordPress currency converter guide for the full walkthrough.
Conclusion
You now have a complete Laravel currency API integration. The service class fetches and caches live rates, the controller passes them to a Blade view, and the Artisan command builds your own rate history on a daily schedule.
The free plan covers real-time latest rates for 1024 currencies, which is enough to get a Laravel application into production with no upfront cost. When you need a custom base currency, historical rates, higher call volume, or faster updates, those sit on the paid plans. If you prefer to start in another language first, the Python currency converter API guide covers the same endpoints.
FAQs
Does CurrencyFreaks work with Laravel 11?
Yes. The Http facade and Cache facade used in this tutorial are available in Laravel 8 through Laravel 11 with no changes to the code. The scheduling syntax is the only difference: Laravel 10 uses app/Console/Kernel.php, and Laravel 11 uses routes/console.php.
Can I fetch historical exchange rates on the free plan?
No. The historical rates endpoint (/rates/historical) is available on paid plans only and returns HTTP 402 on the free plan. The free plan covers real-time latest rates. To build history without a paid plan, store the latest rates daily with the Artisan command in Step 7.
Can I change the base currency from USD?
On the free plan, leave the base as USD, which is the default, and do not send the base parameter. Changing the base currency is a paid feature, and sending a non-USD base on the free plan returns HTTP 402. The service in this guide omits the parameter automatically when the base is USD.
How do I avoid hitting the rate limit on the free plan?
The 60-minute cache in the service class means you make at most 24 API calls per day per currency combination, well within the 1,000 per month free limit. For higher traffic, increase the cache TTL, or use the daily Artisan command to read from your own database instead of the API.
What does the API response look like?
Every response includes a base, a date, and a rates object where each key is a currency code and each value is the exchange rate as a string. Cast the value with (float)$rate in PHP before formatting or calculating.
Start integrating live exchange rates into your Laravel app — sign up at CurrencyFreaks for free.
