# Add currency exchange rates and conversion to PHP

Use PHP cURL for server-side requests and cache the latest response until nextUpdateAt. Read the key from the server environment.

## Get a free API key

Sign up at https://currencyexchangerate-api.com/auth/login, then create a key under API keys in the console. The free plan includes 3,000 requests per month with no credit card. Store the secret as CXR_API_KEY in server-side environment configuration; never expose it in browser code.

File: `.env`

```shell
CXR_API_KEY=cxr_your_secret_key
```

## Create a reusable client and cache rates

Use the standard HTTP client. Cache every /latest response until nextUpdateAt: rates change at most once by then, and reusing the response saves quota. The helper also exposes /convert.

File: `rates.php`

```php
<?php

const API_BASE_URL = 'https://api.currencyexchangerate-api.com/api/v1';

function getApi(string $path): array
{
    $retry = null;
    $ch = curl_init(API_BASE_URL . $path);
    curl_setopt_array($ch, [
        CURLOPT_RETURNTRANSFER => true,
        CURLOPT_HTTPHEADER => [
            'Authorization: Bearer ' . getenv('CXR_API_KEY'),
        ],
        CURLOPT_HEADERFUNCTION => function ($curl, $header) use (&$retry) {
            if (stripos($header, 'Retry-After:') === 0) {
                $retry = (int) trim(substr($header, 12));
            }

            return strlen($header);
        },
    ]);
    $body = curl_exec($ch);
    $status = curl_getinfo($ch, CURLINFO_HTTP_CODE);
    curl_close($ch);

    if ($body === false) {
        throw new RuntimeException('Rates API unreachable');
    }

    $data = json_decode($body, true, 512, JSON_THROW_ON_ERROR);
    if ($status >= 400) {
        $message = $data['detail'] ?? 'Rates API error';
        if ($retry) {
            $message .= " (retry after $retry seconds)";
        }
        throw new RuntimeException($message, $status);
    }

    return $data;
}

function latest(string $base = 'USD', string $symbols = 'EUR,GBP'): array
{
    // Per process; use APCu or a framework cache to share across requests.
    static $saved = [];

    $id = "$base:$symbols";
    if (isset($saved[$id]) && time() < $saved[$id]['until']) {
        return $saved[$id]['data'];
    }

    $query = http_build_query(compact('base', 'symbols'));
    $data = getApi('/latest?' . $query);
    $saved[$id] = ['until' => strtotime($data['nextUpdateAt']), 'data' => $data];

    return $data;
}

function convert(string $from, string $to, float $amount): array
{
    $query = http_build_query(compact('from', 'to', 'amount'));

    return getApi('/convert?' . $query);
}
```

## Use it in PHP

Call the helper from a server route, controller, view, command, or script. Format converted values for the user’s locale.

File: `convert.php`

```php
<?php

require 'rates.php';

$result = convert('EUR', 'GBP', 100);
$formatter = new NumberFormatter('en_GB', NumberFormatter::CURRENCY);
echo $formatter->formatCurrency($result['result'], $result['to']);
```

## Handle API errors

A 401 means the key is invalid. A 429 can mean a rate limit or quota limit; read Retry-After in seconds before retrying. Other errors use application/problem+json.

File: `errors.php`

```php
<?php

require 'rates.php';

try {
    $result = convert('EUR', 'GBP', 100);
} catch (RuntimeException $e) {
    // getCode() is the HTTP status: 401 bad key, 429 limited.
    http_response_code($e->getCode() === 429 ? 429 : 502);
    echo $e->getMessage();
}
```

## Format the amount for display

Use locale-aware currency formatting instead of adding a symbol manually.

File: `format.php`

```php
<?php

// Requires the intl extension.
$formatter = new NumberFormatter('de_DE', NumberFormatter::CURRENCY);
echo $formatter->formatCurrency(85.249105, 'EUR');
```

## Related links

- [API documentation](https://currencyexchangerate-api.com/docs.md)
- [All integration guides](https://currencyexchangerate-api.com/guides.md)
- [Get a free API key](https://currencyexchangerate-api.com/auth/login)
