WooCommerce работает примерно в трети интернет-магазинов мира, и как только один из них хочет продавать через границы, появляется один и тот же вопрос: как удержать честные цены, налоги и выплаты в двенадцати валютах, не проверяя FX-рынок руками каждое утро? Честный ответ — подключить к магазину API курсов валют для WooCommerce в реальном времени, но сделать это правильно тоньше, чем «поставил плагин и забыл». Нужно подумать о частоте обновления, о cache stampede, о совместимости платёжных шлюзов, об округлении и о том, что произойдёт, если апстрим вернёт 503 в середине чекаута.
Это руководство разбирает три рабочих паттерна, которые разработчики реально используют в 2026, с рабочим PHP для каждого. Мы подключим API Finexly к WooCommerce тремя способами: как кастомный фид для существующего плагина-переключателя валют, как фильтр-слой, написанный с нуля, и как задачу Action Scheduler с правильной блокировкой. К концу у вас будет паттерн под любой стек — чистый WooCommerce, WooCommerce с переключателем-плагином или полностью кастомный B2B-стор.
Зачем WooCommerce реальный API курсов валют
Из коробки WooCommerce задаёт одну базовую валюту в woocommerce_currency и предполагает, что любые заказы, возвраты и выплаты считаются в ней. Мультивалютность — это слой сверху: переключатель в шапке, пересчитанные итоги корзины и (иногда) локализованная обработка платежей. Этому слою нужны курсы откуда-то, и «откуда-то» по умолчанию обычно одно из двух — вручную введённый курс, который никто не помнит обновлять, или фид бесплатного провайдера, тикающий раз в 12–24 часа.
В FX-среде 2026 оба варианта опасны. Пары вроде USD/JPY 6 мая сдвинулись на 0,95% после интервенции Токио, а валюты развивающихся рынков регулярно ходят больше 1% внутри дня. Магазин, показывающий 24-часовой курс, продаёт со скидкой или премией 1–2% — и это до спреда, который шлюз накручивает при сеттлменте. Реальный API курсов валют для WooCommerce с разумным кеш-слоем держит отображаемые цены в долях процента от mid-market всегда.
Вторая причина — совместимость платёжных шлюзов. У Stripe, PayPal и большинства крупных шлюзов свои списки поддерживаемых валют сеттлмента. Если магазин показывает цены в 15 валютах, а Stripe умеет сеттлить только в 6, нужно знать, какая конвертация происходит, где и по какому курсу. Этот расчёт намного проще, когда ваш слой владеет источником истины по FX, а не чёрная коробка-шлюз.
Три паттерна интеграции
Подключить валютный API к магазину WooCommerce можно в трёх местах, у каждого свои компромиссы:
- Зацепиться за существующий плагин-переключатель — самый быстрый путь, работает с CURCY, FOX, Aelia, VillaTheme и большинством популярных. Кода минимум, но вы привязаны к ритму обновлений плагина и его UI.
- Свой фильтровый слой через core-хуки WooCommerce — полный контроль над тем, какие цены конвертируются (обычная, со скидкой, доставка, налог), но кода больше, и UI магазина за вами.
- Гибрид: плагин держит UI, ваш код держит курсы — лучшее из двух миров для магазинов уже на плагине-переключателе. Плагин рисует переключатель, ваша задача по расписанию пишет курсы в его хранилище.
Соберём все три.
Паттерн 1: кастомный фид курсов для существующего плагина-переключателя
Большинство популярных WooCommerce-плагинов курсов валют экспонируют фильтр или action-хук для кастомного источника курсов. CURCY от VillaTheme использует wmc_get_currency_exchange_rates. FOX — woocs_currencies_array. Aelia — wc_aelia_cs_exchange_rates_request_args. Паттерн один и тот же: вы перехватываете курсы, которые плагин запросил бы у встроенных провайдеров, и подменяете их своими.
Полный пример для CURCY (та же форма работает для FOX с другим именем фильтра). Положите в маленький mu-plugin или в functions.php темы:
<?php
/**
* Plugin Name: Finexly Rates for CURCY
* Description: Pulls real-time WooCommerce multi-currency exchange rates from Finexly.
*/
add_filter( 'wmc_get_currency_exchange_rates', 'finexly_curcy_rates', 10, 1 );
function finexly_curcy_rates( $rates ) {
$cached = get_transient( 'finexly_curcy_rates' );
if ( false !== $cached ) {
return wp_parse_args( $cached, $rates );
}
$base = get_woocommerce_currency(); // e.g. "USD"
$symbols = array_keys( $rates ); // e.g. ["EUR","GBP","JPY",...]
$api_key = defined( 'FINEXLY_API_KEY' ) ? FINEXLY_API_KEY : '';
if ( empty( $api_key ) || empty( $symbols ) ) {
return $rates; // graceful fall-through to plugin defaults
}
$url = add_query_arg( array(
'apikey' => $api_key,
'base' => $base,
'currencies' => implode( ',', $symbols ),
), 'https://api.finexly.com/v1/latest' );
$response = wp_remote_get( $url, array( 'timeout' => 8 ) );
if ( is_wp_error( $response ) || 200 !== wp_remote_retrieve_response_code( $response ) ) {
return $rates;
}
$body = json_decode( wp_remote_retrieve_body( $response ), true );
if ( empty( $body['rates'] ) ) {
return $rates;
}
// CURCY expects an associative array of CODE => rate (relative to base).
$fresh = array();
foreach ( $body['rates'] as $code => $rate ) {
$fresh[ $code ] = (float) $rate;
}
set_transient( 'finexly_curcy_rates', $fresh, HOUR_IN_SECONDS );
return wp_parse_args( $fresh, $rates );
}Несколько важных деталей. Во-первых, вызов wp_remote_get имеет таймаут 8 секунд — достаточно длинный, чтобы поглотить медленный ответ, и достаточно короткий, чтобы PHP-FPM не лёг при деградации апстрима. Во-вторых, при любой неудаче (сетевая ошибка, не 200, отсутствие payload) мы возвращаем дефолтные $rates плагина, а не пустой массив. Деградировавший чекаут лучше сломанного. В-третьих, transient кеширует успешные ответы на час — для ритейла этого с запасом, и квота API остаётся под контролем.
Объявите ключ в wp-config.php:
define( 'FINEXLY_API_KEY', 'your_api_key_here' );Готово. Переключатель CURCY рисуется в шапке, покупатель выбирает валюту, цены пересчитываются по свежим курсам из вашего API-вызова, а не по дефолтному 12-часовому фиду Yahoo Finance, которым пользуется CURCY.
Паттерн 2: мультивалютность с нуля через core-фильтры WooCommerce
Если плагин в цепочке не нужен — или вы строите кастомный B2B-магазин, где переключатель валют относится к настройкам аккаунта, а не к выпадайке в шапке, — весь мультивалютный слой можно собрать на двух core-фильтрах: woocommerce_currency и raw_woocommerce_price.
woocommerce_currency позволяет менять активную валюту запросом. raw_woocommerce_price позволяет преобразовать любую цену, которую отображает WooCommerce. Соединяете с переменной сессии — и получаете рабочий переключатель примерно за 60 строк:
<?php
class Finexly_WC_Currency {
const SESSION_KEY = 'finexly_active_currency';
const TRANSIENT = 'finexly_rates';
public function __construct() {
add_action( 'init', array( $this, 'maybe_switch_currency' ) );
add_filter( 'woocommerce_currency', array( $this, 'active_currency' ) );
add_filter( 'raw_woocommerce_price', array( $this, 'convert_price' ) );
add_filter( 'woocommerce_package_rates', array( $this, 'convert_shipping' ), 10, 2 );
}
public function maybe_switch_currency() {
if ( isset( $_GET['set_currency'] ) ) {
$code = strtoupper( sanitize_text_field( $_GET['set_currency'] ) );
if ( array_key_exists( $code, $this->rates() ) ) {
WC()->session->set( self::SESSION_KEY, $code );
}
}
}
public function active_currency( $default ) {
$code = WC()->session ? WC()->session->get( self::SESSION_KEY ) : null;
return $code ?: $default;
}
public function convert_price( $price ) {
$code = $this->active_currency( get_option( 'woocommerce_currency' ) );
$rates = $this->rates();
if ( ! isset( $rates[ $code ] ) ) {
return $price;
}
return (float) $price * (float) $rates[ $code ];
}
public function convert_shipping( $rates, $package ) {
$code = $this->active_currency( get_option( 'woocommerce_currency' ) );
$fx = $this->rates();
if ( ! isset( $fx[ $code ] ) ) {
return $rates;
}
foreach ( $rates as $rate ) {
$rate->cost = (float) $rate->cost * (float) $fx[ $code ];
foreach ( $rate->taxes as &$tax ) {
$tax = (float) $tax * (float) $fx[ $code ];
}
}
return $rates;
}
private function rates() {
$cached = get_transient( self::TRANSIENT );
return is_array( $cached ) ? $cached : array();
}
}
new Finexly_WC_Currency();Transient заполняет отдельная задача по расписанию (следующий раздел), поэтому этот класс никогда не делает сетевой вызов внутри запроса — каждая загрузка страницы это один get_transient-лукап, в окружении с object cache он стоит ~0,1 мс. Это разница между +8 мс TTFB на каждой некешированной странице и +800 мс раз в час одному воркеру cron.
Две вещи на продакшен: точность и округление. WooCommerce хранит цены в decimal; умножив decimal(13,4) на курс 1.10851234 и сохранив обратно в decimal(13,4), вы тихо потеряете точность. Для дешёвых позиций (SKU дешевле $1, копилки чаевых, микро-донаты) держите внутри минимум 8 знаков точности и округляйте до естественной мин-единицы валюты только на слое отображения. Хелпер wc_price() сделает округление при отображении за вас; вам остаётся хранить достаточно широкие float'ы.
Паттерн 3: задача обновления Action Scheduler с защитой от stampede
Оба паттерна выше читают из transient. Кто-то должен в transient писать. В WooCommerce правильный инструмент — Action Scheduler, исполнитель задач, идущий в комплекте с WooCommerce: он надёжен, ретраит автоматически и обрабатывает конкурентность лучше, чем wp_schedule_event.
<?php
/**
* Plugin Name: Finexly Rates Refresh
* Description: Refreshes WooCommerce multi-currency exchange rates from Finexly hourly.
*/
add_action( 'init', function () {
if ( false === as_next_scheduled_action( 'finexly_refresh_rates' ) ) {
as_schedule_recurring_action(
time() + 60,
HOUR_IN_SECONDS,
'finexly_refresh_rates',
array(),
'finexly'
);
}
} );
add_action( 'finexly_refresh_rates', 'finexly_do_refresh' );
function finexly_do_refresh() {
// Stampede lock: only one worker at a time.
$lock_key = 'finexly_refresh_lock';
if ( get_transient( $lock_key ) ) {
return;
}
set_transient( $lock_key, 1, 30 );
try {
$base = get_option( 'woocommerce_currency', 'USD' );
$supported = apply_filters( 'finexly_currencies', array(
'USD','EUR','GBP','JPY','CHF','CAD','AUD','CNY','INR','BRL','MXN','SEK','NOK','SGD','HKD','TRY','ZAR','PLN'
) );
$url = add_query_arg( array(
'apikey' => FINEXLY_API_KEY,
'base' => $base,
'currencies' => implode( ',', $supported ),
), 'https://api.finexly.com/v1/latest' );
$response = wp_remote_get( $url, array( 'timeout' => 10 ) );
if ( is_wp_error( $response ) || 200 !== wp_remote_retrieve_response_code( $response ) ) {
// Don't blow away existing rates on a transient upstream failure.
error_log( 'Finexly refresh failed: ' . wp_remote_retrieve_response_code( $response ) );
return;
}
$body = json_decode( wp_remote_retrieve_body( $response ), true );
if ( empty( $body['rates'] ) ) {
return;
}
$rates = array_map( 'floatval', $body['rates'] );
$rates[ $base ] = 1.0;
set_transient( 'finexly_rates', $rates, 3 * HOUR_IN_SECONDS );
// Persist to options as a durable fallback. Object cache flushes
// would otherwise wipe transients and force a cold checkout.
update_option( 'finexly_rates_persistent', array(
'fetched_at' => time(),
'base' => $base,
'rates' => $rates,
), false );
} finally {
delete_transient( $lock_key );
}
}Три приёма стоит отметить. Блокировка от stampede не даёт двум параллельным воркерам долбить апстрим, когда Action Scheduler запускается с задержкой и ставит две задачи подряд. TTL transient в три раза больше интервала обновления — намеренный буфер: одна неудачная попытка не сносит кэш до следующей. Постоянный фолбэк в options — третий слой: если object cache по любой причине сбросится, у магазина останется набор курсов до 24 часов давности, и метод rates() может вернуться на него прежде, чем отдать пусто.
Если интересна сама архитектура кеширования глубже, паттерны из нашего гида по кешу и обработке ошибок для API курсов валют переносятся на WooCommerce без правок.
Тесты вашей мультивалютной интеграции
Магазины WooCommerce ломаются в проде тремя предсказуемыми способами, и все три тестируемы. Минимальный PHPUnit-сьют на WP_Mock и Mockery:
<?php
class Finexly_WC_Currency_Test extends WP_Mock\Tools\TestCase {
public function test_convert_price_with_known_rate() {
WP_Mock::userFunction( 'get_transient' )
->with( 'finexly_rates' )
->andReturn( array( 'EUR' => 0.92, 'GBP' => 0.79 ) );
WP_Mock::userFunction( 'get_option' )
->with( 'woocommerce_currency' )
->andReturn( 'USD' );
$svc = new Finexly_WC_Currency();
$svc->set_active_currency( 'EUR' );
$this->assertEqualsWithDelta( 92.0, $svc->convert_price( 100.0 ), 0.0001 );
}
public function test_returns_original_price_when_rate_missing() {
WP_Mock::userFunction( 'get_transient' )
->andReturn( array() );
$svc = new Finexly_WC_Currency();
$svc->set_active_currency( 'XYZ' ); // unsupported code
$this->assertEquals( 100.0, $svc->convert_price( 100.0 ) );
}
public function test_refresh_handles_upstream_500() {
WP_Mock::userFunction( 'wp_remote_get' )
->andReturn( array( 'response' => array( 'code' => 500 ) ) );
WP_Mock::userFunction( 'set_transient' )->never();
WP_Mock::userFunction( 'update_option' )->never();
finexly_do_refresh();
// Test passes if no fatal error and we don't overwrite cached rates.
}
}Третий тест чаще всего пропускают. Почти каждый валютный баг WooCommerce, который я видел в проде, сводится к «апстрим вернул ошибку, и мы закешировали пустой ответ». Проверять, что вы не пишете мусор при сбое, ценнее, чем проверять, что вы пишете правильное при успехе.
Распространённые ловушки и как их обходить
Поддержка валют шлюзом. То, что магазин показывает цены в TRY, не значит, что Stripe сеттлится в TRY. Перед тем как показать валюту в переключателе, проверяйте WC()->payment_gateways() и фильтруйте по пересечению валют, поддерживаемых активными шлюзами. Сравнение Stripe FX Quotes API и выделенного API курсов валют подробно разбирает компромиссы.
Возвраты и редактирование заказа. Возврат в EUR через три недели после исходного заказа в USD-базе пойдёт не по сегодняшнему курсу — пойдёт по тому, что шлюз заберёт в момент возврата. Сохраняйте курс на момент заказа в meta заказа (update_post_meta( $order_id, '_finexly_rate', $rate )), чтобы reporting мог сводить. Это же полезно для слоя интеграции с бухгалтерским ПО.
Округление. Округляйте при отображении, не при сохранении. Хранить 9.99 * 0.92 = 9.1908, округлённое до 9.19, а потом вернуть в USD — получите 9.989..., не 9.99. Широкая внутренняя точность плюс округление при показе — единственный здоровый паттерн.
Корректность ISO 4217. Не верьте пользовательскому вводу. Валидируйте все коды валют по списку ISO 4217, прежде чем класть в сессию.
Какой API выбрать: бесплатный или платный
Встроенные провайдеры WooCommerce (бесплатный уровень Open Exchange Rates, депрекейтнутый CurrencyLayer и т.д.) обновляются раз в 12–24 часа и упираются в маленькие квоты. Для маленького магазина с одним товаром и десятком заказов в месяц нормально; для всего, что с трафиком, нужен выделенный провайдер минимум с часовым обновлением и квотой, которая не заставляет считать каждый вызов. В нашем сравнении бесплатных и платных API курсов валют 2026 есть конкретные цифры.
API Finexly как раз под этот сценарий — 170+ валют, обновление за 60 секунд по майорам, ежечасное по long-tail и бесплатный уровень 1000 запросов/мес, которого хватает на одну задачу Action Scheduler в час с запасом на ретраи. Можно сравнить API курсов валют бок о бок или зарегистрироваться бесплатно, чтобы попробовать в dev-магазине.
Часто задаваемые вопросы
WooCommerce поддерживает мультивалютность из коробки?
Частично. WooCommerce поддерживает одну базовую валюту для заказов и выплат. Мультивалютное отображение и чекаут требуют либо плагина-переключателя (CURCY, FOX, Aelia и др.), либо кастомного кода с фильтрами woocommerce_currency и raw_woocommerce_price, и в обоих случаях источник курсов поставляете вы.
Как часто обновлять курсы в WooCommerce? Для ритейла достаточно раз в час. Для B2B с крупными счетами безопаснее каждые 15–30 минут. Агрессивнее редко окупается: платёжные шлюзы всё равно перекотируют при сеттлменте, поэтому разница между 5- и 30-минутным курсом тонет в спреде.
Можно ли подключить кастомный API курсов к плагину-переключателю, который уже стоит?
Да. CURCY экспонирует wmc_get_currency_exchange_rates, FOX — woocs_currencies_array, у Aelia есть wc_aelia_cs_exchange_rates_request_args. Подвешиваетесь к фильтру, возвращаете свои курсы, UI плагина продолжает работать как было.
Что будет, если API курсов недоступен во время чекаута? С архитектурой выше клиент ничего не заметит. Слой цен читает transient, который наполняет фоновая задача; даже если последний refresh упал, предыдущий курс всё ещё в кеше, а постоянный option хранит 24-часовой фолбэк. Чекаут идёт по последнему известному корректному курсу.
Как быть с возвратами через разные валюты? Сохраняйте курс на момент заказа в meta заказа. При возврате считайте сумму по тому самому курсу, не по сегодняшнему — клиент должен получить компенсацию в той валюте, в которой платил. P&L по FX бухгалтерия сводит отдельно.
Подводя итог
Крепкая интеграция API курсов валют для WooCommerce на самом деле не про вызов API — она про четыре слоя вокруг него: кеш, расписание, фолбэки, тесты. Соберёте эти четыре правильно — магазин будет держать честные цены в 50+ валютах без единого ночного пейджера.
Готовы подключить курсы валют в реальном времени к WooCommerce? Возьмите бесплатный API-ключ Finexly — без карты. Стартуйте с 1000 бесплатных запросов в месяц, бросьте сниппеты выше в mu-plugin — и мультивалютность будет в проде до обеда. Когда вырастете из бесплатного уровня, тарифы масштабируются линейно, не экспоненциально.
Explore More
Vlado Grigirov
Senior Currency Markets Analyst & Financial Strategist
Vlado Grigirov is a senior currency markets analyst and financial strategist with over 14 years of experience in foreign exchange markets, cross-border finance, and currency risk management. He has wo...
View full profile →