---
title: "Exchange Rates - Multi-Currency for WooCommerce - PerfLocale"
description: "Scheduled exchange-rate syncing for PerfLocale multi-currency WooCommerce. No rate provider is bundled — you supply rates through a filter."
canonical: "https://perflocale.com/docs/exchange-rates/"
source: "https://perflocale.com/docs/exchange-rates/"
format: "markdown"
---
# Exchange Rates

PerfLocale can refresh your per-language currency rates on a schedule. The scheduling, the cross-calculation, the circuit breaker and the admin UI are all in the plugin — but **no rate provider ships with it**. Until your site wires one up, PerfLocale contacts no exchange-rate service at all, and the Auto-Sync fields say so.

## Quick Start

1.  Register a rate source (see [Supplying rates](#supplying-rates) below) — this step is not optional; nothing is bundled.
2.  Go to **PerfLocale > Settings > Addons > WooCommerce**
3.  Enable **Per-Language Currency**
4.  Configure a currency code for each language (the code is auto-detected from the language locale — `pl_PL → PLN`, `en_GB → GBP`, etc. — on first add)
5.  Enable **Auto-Sync Rates**
6.  Select your registered provider and a sync interval
7.  Click **Sync Now** to verify — the per-language rate inputs update to the freshly-fetched values and persist across page reloads. WooCommerce cart / checkout / mini-cart price conversion uses the same synced rates at runtime.

**Per-row manual override.** Setting `manual_rate => true` on a specific language entry (programmatically, or via `wp option patch update perflocale_settings wc_currencies <slug> manual_rate 1`) pins that row to its manually-entered value and excludes it from the auto-sync feed, even when global Auto-Sync is on. Useful for hedging contracts or B2B fixed rates. If you never wire up a provider, every row is effectively a manual rate — multi-currency display still works, the numbers just don’t move on their own.

## Supplying rates

PerfLocale bundles no exchange-rate service. Earlier builds shipped several; they were removed so that installing the plugin never adds an outbound dependency you didn’t ask for. Two filter seams remain, and you need exactly one of them:

-   `perflocale/woocommerce/exchange_rate_providers` — register a provider with a `fetch_callback`. It gets the scheduling, the provider dropdown, the “Sync Now” button, the last-sync status line and the circuit breaker for free. Best when you’re calling a rate API.
-   `perflocale/woocommerce/exchange_rates_fetched` — return the rate array directly. Best when the numbers come from somewhere that isn’t an HTTP call: your ERP, a treasury feed, a hard-coded contract table.

If neither is wired, a sync run logs _“No exchange-rate provider configured”_ and stops without making a request. If you supply rates through `exchange_rates_fetched` alone, leave the provider dropdown empty — the sync still runs, because PerfLocale checks for that filter before bailing out.

Choosing a provider is a decision about privacy and terms as much as accuracy: whatever you register is a third-party service your store now talks to on a schedule, and its terms and privacy policy are yours to disclose, not ours.

## Sync Intervals

| Interval | Requests/day | Requests/month |
| --- | --- | --- |
| Every Hour | 24 | ~720 |
| Every 2 Hours | 12 | ~360 |
| Every 4 Hours | 6 | ~180 |
| Every 6 Hours | 4 | ~120 |
| Every 8 Hours | 3 | ~90 |
| Every 12 Hours | 2 | ~60 |
| Once Daily (default) | 1 | ~30 |
| Once Weekly | ~0.14 | ~4 |
| Once Monthly | ~0.03 | 1 |

Choose an interval that fits within your API plan limits. For most stores, **daily** is sufficient since exchange rates don't change drastically within a day.

## API Key Management

Because no rate provider is bundled, PerfLocale defines no exchange-rate API-key constants. (The seven credentials that _do_ have canonical env-var / constant names are all machine-translation ones — see [API Keys: Environment Variables & Constants](https://perflocale.com/docs/api-key-constants/).)

A provider registered through the filter can declare `needs_key` and `key_setting`. PerfLocale then reads `perflocale_settings[key_setting]` before each sync and refuses to run when it’s empty (“API key missing” in the log). It does _not_ render an input for that key — the admin key fields disappeared along with the bundled providers — so if you use `key_setting`, populate it yourself, e.g. `wp option patch update perflocale_settings my_rates_key '…'`.

### Keeping the key out of the database

The simpler and safer route is to skip `key_setting` entirely and read your own environment variable or constant inside `fetch_callback`:

```php
'needs_key'      => false, // nothing read from, or stored in, settings
'fetch_callback' => function ( string $base, array $targets, string $api_key ): array {
	$key = getenv( 'MY_RATES_API_KEY' ) ?: ( defined( 'MY_RATES_API_KEY' ) ? MY_RATES_API_KEY : '' );
	// ...call your API with $key, return [ 'EUR' => 0.92, ... ]
},
```

### If the key does live in the database

Anything stored in `perflocale_settings` whose name ends in `_api_key`, `_token`, `_key`, `_secret` or `_password` is **redacted from PerfLocale’s settings export**, so a custom provider’s key is covered automatically if you name it that way. Importing a redacted export leaves the live credential in place rather than blanking it; on a fresh site you re-enter the key by hand.

Reasons to prefer your own env var / constant over a stored setting:

-   Not stored in the database (not exposed in backups or SQL dumps)
-   Not visible in the WordPress admin UI
-   Can differ per environment (staging vs production)
-   Cannot be changed by other plugins or admin users

## Custom Provider via Filter

Register your exchange rate provider using the `perflocale/woocommerce/exchange_rate_providers` filter. This is the seam that used to hold the bundled providers, so the shape is unchanged:

```php
add_filter( 'perflocale/woocommerce/exchange_rate_providers', function( array $providers ): array {
	$providers['my_custom_api'] = [
		'name'           => 'My Custom API',
		'needs_key'      => true,
		'key_setting'    => 'wc_my_custom_key',
		'fetch_callback' => function( string $base_currency, array $target_currencies, string $api_key ): array {
			$response = wp_remote_get( 'https://my-api.example.com/rates?base=' . $base_currency . '&key=' . $api_key );

			if ( is_wp_error( $response ) ) {
				return [];
			}

			$body = json_decode( wp_remote_retrieve_body( $response ), true );

			$rates = [];
			foreach ( $target_currencies as $code ) {
				if ( isset( $body['rates'][ $code ] ) ) {
					$rates[ $code ] = (float) $body['rates'][ $code ];
				}
			}

			return $rates;
		},
	];

	return $providers;
} );
```

Once registered, the provider appears in the **Exchange Rate Provider** dropdown on the WooCommerce settings subtab (which otherwise just explains that nothing is registered). `fetch_callback` receives the store’s base currency, the target codes derived from your per-language currency table, and the value of `key_setting` — an empty string when `needs_key` is false. Return `[ 'EUR' => 0.92, … ]`; return `[]` on failure and PerfLocale feeds the `fx_sync` circuit breaker, which skips subsequent ticks for a cooldown instead of retrying into an outage.

## Hooks Reference

### Filters

#### `perflocale/woocommerce/exchange_rate_providers`

Register the exchange rate providers available to the store. The default list is empty.

```php
add_filter( 'perflocale/woocommerce/exchange_rate_providers', function( $providers ) {
	// Add a provider (see example above)
	$providers['my_api'] = [ ... ];

	// Remove one another plugin registered
	unset( $providers['someone_elses'] );

	return $providers;
} );
```

#### `perflocale/woocommerce/exchange_rates_fetched`

Filter rates after they are fetched but before they are saved.

```php
add_filter( 'perflocale/woocommerce/exchange_rates_fetched', function( array $rates, string $base, string $provider ): array {
	// Add a margin to all rates
	foreach ( $rates as $code => $rate ) {
		$rates[ $code ] = $rate * 1.02; // 2% markup
	}
	return $rates;
}, 10, 3 );
```

### Actions

#### `perflocale/woocommerce/exchange_rates_synced`

Fires after rates are successfully saved.

```php
add_action( 'perflocale/woocommerce/exchange_rates_synced', function( array $rates, string $base, string $provider ): void {
	// Log the sync
	error_log( sprintf( 'Exchange rates synced from %s: %s', $provider, wp_json_encode( $rates ) ) );

	// Clear page cache
	if ( function_exists( 'wp_cache_flush' ) ) {
		wp_cache_flush();
	}
}, 10, 3 );
```

## Troubleshooting

### Rates not updating

1.  Check that WP-Cron is running. Some hosts disable WP-Cron; you may need a server-side cron job:
    
    ```
    */5 * * * * curl -s https://example.com/wp-cron.php > /dev/null 2>&1
    ```
    
2.  Click **Sync Now** in the settings to test manually.
3.  Enable `WP_DEBUG` and `WP_DEBUG_LOG` in `wp-config.php` to see sync errors:
    
    ```
    define( 'WP_DEBUG', true );
    define( 'WP_DEBUG_LOG', true );
    ```
    
    Check `wp-content/debug.log` for lines starting with `PerfLocale Exchange Rate Sync:`.

### "No exchange-rate provider configured"

Expected on a fresh install — nothing is bundled. Register one through `perflocale/woocommerce/exchange_rate_providers` and select it in the dropdown, or return rates straight from `perflocale/woocommerce/exchange_rates_fetched` and leave the dropdown empty.

### "API key missing" error

Your provider declared `needs_key` with a `key_setting`, and that setting is empty. PerfLocale renders no input for it, so write the value yourself (`wp option patch update perflocale_settings <key_setting> '…'`) — or drop `needs_key` and resolve the credential inside `fetch_callback`.

### Rates differ from expected

-   Free API tiers often return the previous day’s rates.
-   Cross-calculation (when your source’s base differs from your store base) introduces minor rounding.
-   Different data sources disagree slightly; two providers rarely return identical numbers.

### "No rates returned" error

-   Your `fetch_callback` (or the `exchange_rates_fetched` filter) returned an empty array — the log line names which one.
-   Check that your target currency codes are valid ISO 4217 codes (e.g., EUR, GBP, JPY).
-   Repeated empty results — or a response whose rates are all non-numeric or non-positive — trip the `fx_sync` circuit breaker; the next few scheduled ticks are skipped until it cools down.
