Назад к блогу

Мультивалютность в WooCommerce: интеграция API курсов валют в реальном времени (Руководство для разработчиков 2026)

V
Vlado Grigirov
May 07, 2026
WooCommerce Currency API Exchange Rates WordPress PHP E-commerce Finexly

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 можно в трёх местах, у каждого свои компромиссы:

  1. Зацепиться за существующий плагин-переключатель — самый быстрый путь, работает с CURCY, FOX, Aelia, VillaTheme и большинством популярных. Кода минимум, но вы привязаны к ритму обновлений плагина и его UI.
  2. Свой фильтровый слой через core-хуки WooCommerce — полный контроль над тем, какие цены конвертируются (обычная, со скидкой, доставка, налог), но кода больше, и UI магазина за вами.
  3. Гибрид: плагин держит 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 — и мультивалютность будет в проде до обеда. Когда вырастете из бесплатного уровня, тарифы масштабируются линейно, не экспоненциально.

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 →