Volver al blog

WooCommerce Multidivisa: Cómo Integrar una API de Tipos de Cambio en Tiempo Real (Guía para Desarrolladores 2026)

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

WooCommerce impulsa aproximadamente un tercio de las tiendas online del mundo, y en cuanto una de esas tiendas decide vender al extranjero aparece la misma pregunta: ¿cómo mantener precios, impuestos y liquidaciones honestos en doce divisas sin revisar el mercado de divisas a mano cada mañana? La respuesta sincera es conectar una API de tipos de cambio multidivisa para WooCommerce a tu tienda — pero hacerlo bien tiene más matices que instalar un plugin y olvidarse. Hay que pensar en la cadencia de actualización, en las cargas concurrentes (cache stampedes), en la compatibilidad con la pasarela de pago, en el redondeo y en qué pasa cuando el proveedor upstream devuelve un 503 a mitad de un checkout.

Esta guía recorre tres patrones de producción que los desarrolladores realmente usan en 2026, con código PHP funcional para cada uno. Vamos a conectar la API de Finexly con WooCommerce de tres formas distintas: como feed personalizado para un plugin de selector de divisas existente, como una capa basada en filtros desarrollada desde cero y como una tarea de Action Scheduler con un bloqueo correcto. Al terminar tendrás un patrón que encaja con cualquier stack — WooCommerce limpio, WooCommerce con un plugin de switcher, o un build B2B totalmente personalizado.

Por Qué una API de Tipos de Cambio en Tiempo Real Importa en WooCommerce

De fábrica, WooCommerce define una única divisa base en woocommerce_currency y asume que cada pedido, devolución y liquidación se hace en esa divisa. El soporte multidivisa es una capa que se monta encima: un selector en la cabecera, totales recalculados en el carrito y (a veces) procesamiento de pago localizado. Esa capa necesita tipos de cambio de algún sitio, y "algún sitio" suele ser por defecto una de dos cosas — una tasa introducida a mano que nadie se acuerda de actualizar o un feed de proveedor gratuito que se mueve cada 12-24 horas.

Las dos opciones son peligrosas en el entorno FX de 2026. Pares como USD/JPY se movieron 0,95% solo el 6 de mayo tras la intervención de Tokio, y las divisas de mercados emergentes oscilan rutinariamente más de un 1% intradía. Una tienda mostrando una tasa con 24 horas de antigüedad está vendiendo con un descuento o sobreprecio del 1-2% según el viento que sople — y eso antes de contar el spread que la pasarela añade en la liquidación. Una API de tipos de cambio multidivisa para WooCommerce en tiempo real con una capa de caché sensata mantiene tus precios mostrados a fracciones de un porcentaje del mid-market, siempre.

La otra razón por la que esto importa: la compatibilidad de la pasarela de pago. Stripe, PayPal y la mayoría de las pasarelas grandes tienen su propia lista de divisas de liquidación soportadas. Si tu tienda muestra precios en 15 divisas pero Stripe solo liquida en 6, necesitas saber qué conversión ocurre, dónde y a qué tasa — y ese cálculo es mucho más fácil cuando tu capa es la fuente de verdad para el FX, no una pasarela tipo caja negra.

Los Tres Patrones de Integración

Hay tres lugares donde puedes conectar una API de divisas a una tienda WooCommerce, y cada uno tiene compensaciones distintas:

  1. Engancharse a un plugin de switcher existente — el camino más rápido, funciona con CURCY, FOX, Aelia, VillaTheme y la mayoría de plugins populares. La superficie de código es mínima pero quedas atado a la cadencia de actualización del plugin y a su UI de tienda.
  2. Capa propia basada en filtros usando hooks core de WooCommerce — control total sobre qué precios se convierten (regular, oferta, envío, impuestos), pero escribes más código y eres el responsable de la UI de tienda.
  3. Híbrido: el plugin gestiona la UI, tu código gestiona las tasas — lo mejor de ambos mundos para tiendas que ya usan un plugin de switcher. El plugin renderiza el switcher; tu trabajo programado mete las tasas en el almacenamiento del plugin.

Vamos a construir los tres.

Patrón 1: Feed Personalizado para un Plugin de Switcher Existente

La mayoría de los plugins populares de divisas para WooCommerce exponen un filtro o action hook para fuentes de tasas personalizadas. El plugin CURCY de VillaTheme usa wmc_get_currency_exchange_rates. FOX usa woocs_currencies_array. Aelia usa wc_aelia_cs_exchange_rates_request_args. El patrón es el mismo en todos — interceptas las tasas que el plugin obtendría de sus proveedores integrados y sustituyes las tuyas.

Aquí tienes un ejemplo completo para CURCY (la misma forma funciona para FOX cambiando el nombre del filtro). Coloca esto en un mu-plugin o en el functions.php de tu tema:

<?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 );
}

Algunos detalles importantes. Primero, la llamada wp_remote_get tiene un timeout de 8 segundos — bastante para absorber una respuesta lenta, lo bastante corto para que PHP-FPM no caiga si el upstream está degradado. Segundo, ante cualquier fallo (error de red, no-200, payload vacío) devolvemos los $rates por defecto del plugin en lugar de un array vacío. Un checkout degradado es mejor que uno roto. Tercero, el transient cachea las respuestas exitosas durante una hora, lo cual es más que fresco para retail y te mantiene cómodo bajo cualquier cuota de API.

Define tu clave en wp-config.php:

define( 'FINEXLY_API_KEY', 'your_api_key_here' );

Y ya está. El switcher de CURCY se renderiza en la cabecera, los clientes eligen una divisa y los precios se recalculan usando tasas frescas de tu llamada a la API en lugar del feed por defecto de Yahoo Finance que CURCY actualiza cada 12 horas.

Patrón 2: Multidivisa Desde Cero con Filtros Core de WooCommerce

Si no quieres un plugin en medio — o estás construyendo una tienda B2B personalizada donde el switcher forma parte de un setting a nivel de cuenta y no un dropdown de cabecera — puedes construir toda la capa multidivisa con dos filtros core: woocommerce_currency y raw_woocommerce_price.

woocommerce_currency te permite cambiar la divisa activa por petición. raw_woocommerce_price te permite transformar cada precio que WooCommerce muestra. Combínalos con una variable de sesión y tienes un switcher de divisas funcional en unas 60 líneas:

<?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();

El transient lo rellena un job programado aparte (siguiente sección), por lo que esta clase nunca hace una llamada de red dentro de una request — cada carga de página es una sola consulta get_transient, que con object cache habilitado tarda ~0,1 ms. Esa es la diferencia entre añadir 8 ms al TTFB en cada página no cacheada y añadir 800 ms una vez por hora a un solo worker de cron.

Dos cosas a añadir para producción: precisión y redondeo. WooCommerce guarda precios como decimales; multiplicar un valor decimal(13,4) por una tasa 1,10851234 y guardar el resultado como decimal(13,4) recortará precisión silenciosamente. Para artículos de bajo precio (SKUs sub-$1, botes de propinas, micro-donaciones) quieres al menos 8 decimales de precisión interna, redondeando a la unidad mínima natural de la divisa solo en la capa de visualización. El helper wc_price() de WooCommerce gestiona el redondeo de display por ti; lo que tienes que hacer tú es mantener los floats subyacentes con suficiente ancho.

Patrón 3: Job de Refresh con Action Scheduler y Protección Contra Stampedes

Los dos patrones anteriores leen de un transient. Algo tiene que escribir en ese transient. La herramienta correcta en WooCommerce es Action Scheduler — el ejecutor de jobs que WooCommerce trae integrado — porque es fiable, reintenta automáticamente y gestiona la concurrencia mejor que 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 );
    }
}

Tres patrones merecen mención. El stampede lock evita que dos workers paralelos machaquen el upstream cuando Action Scheduler se ejecuta tarde y encola dos jobs seguidos. El TTL del transient ×3 vs. intervalo de refresh de 1 hora es un buffer deliberado — si un refresh falla, el valor cacheado sobrevive hasta el siguiente intento. El fallback persistente en options es la tercera capa: si el object cache se vacía por cualquier razón, tu tienda sigue teniendo un set de tasas usable de hace hasta 24 horas, y puedes hacer que tu método rates() recurra a él antes de devolver vacío.

Si quieres profundizar en la arquitectura de caché, los patrones de nuestra guía de caché y manejo de errores en APIs de divisas se traducen directamente a WooCommerce.

Cómo Probar Tu Integración Multidivisa

Las tiendas WooCommerce se rompen en producción de tres formas predecibles, y las tres son testeables. Aquí tienes una suite mínima con PHPUnit usando WP_Mock y 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.
    }
}

El tercer test es el que más equipos saltan. Casi todos los bugs de divisas en WooCommerce que he visto en producción se reducen a "el upstream devolvió un error y cacheamos la respuesta vacía". Asegurarte de que no escribes basura ante un fallo es más valioso que asegurarte de que escribes lo correcto en éxito.

Errores Frecuentes y Cómo Evitarlos

Compatibilidad de divisas de la pasarela. Que tu tienda muestre precios en TRY no implica que Stripe pueda liquidar en TRY. Antes de mostrar una divisa en tu switcher, comprueba WC()->payment_gateways() y filtra a la intersección de divisas que tus pasarelas activas soportan. La comparativa entre la API de cotizaciones FX de Stripe y una API de divisas dedicada cubre las compensaciones en detalle.

Devoluciones y ediciones de pedido. Una devolución emitida en EUR tres semanas después del pedido original en base USD no usará la tasa de hoy — usará la que la pasarela capture en el momento de la devolución. Guarda la tasa al momento del pedido en metadatos del order (update_post_meta( $order_id, '_finexly_rate', $rate )) para que el reporting pueda reconciliar. Esto también es útil para la capa de integración con software contable.

Redondeo. Redondea al mostrar, no al guardar. Guardar 9.99 * 0.92 = 9,1908 redondeado a 9,19 y luego convertirlo de vuelta a USD te da 9,989..., no 9,99. Precisión interna amplia más redondeo en display es el único patrón sano.

Validez ISO 4217. No confíes en input del usuario. Valida todos los códigos de divisa contra la lista ISO 4217 antes de guardarlos en sesión.

Eligiendo la API: Gratis vs. de Pago

Los proveedores integrados de WooCommerce (la capa gratuita de Open Exchange Rates, la deprecada CurrencyLayer, etc.) actualizan cada 12-24 horas y limitan a cuotas pequeñas. Para una tienda pequeña con un producto y diez pedidos al mes vale; para cualquier cosa con tráfico, vas a querer un proveedor dedicado con al menos refresh por hora y una cuota que no te obligue a racionar llamadas. Nuestra comparativa de APIs de divisas gratis vs. de pago para 2026 repasa los números reales.

La API de Finexly apunta específicamente a este caso de uso — 170+ divisas, refresh de 60 segundos en majors, horario en long-tail y un plan gratis de 1.000 peticiones/mes que basta para un job de Action Scheduler por hora con margen para reintentos. Puedes comparar APIs de divisas lado a lado o registrarte gratis para probarla en tu tienda de desarrollo.

Preguntas Frecuentes

¿WooCommerce soporta multidivisa de forma nativa? Parcialmente. WooCommerce soporta una única divisa base para pedidos y liquidaciones. La visualización multidivisa y el checkout requieren o bien un plugin de switcher (CURCY, FOX, Aelia, etc.) o código personalizado usando los filtros woocommerce_currency y raw_woocommerce_price, y cualquiera de los enfoques necesita una fuente de tasas que tú le proporciones.

¿Con qué frecuencia debería refrescar las tasas en WooCommerce? Para retail e-commerce, cada hora es más que suficiente. Para tiendas B2B con facturas de alto valor, cada 15-30 minutos es más seguro. Más agresivo que eso rara vez compensa la cuota de API — las pasarelas de pago vuelven a cotizar en liquidación de todos modos, así que la diferencia entre una tasa de 5 minutos y una de 30 es ruido frente al spread de la pasarela.

¿Puedo integrar una API de divisas personalizada con el plugin de switcher que ya uso? Sí. CURCY expone wmc_get_currency_exchange_rates, FOX expone woocs_currencies_array, Aelia tiene wc_aelia_cs_exchange_rates_request_args. Engánchate al filtro, devuelve tus tasas y la UI del plugin sigue funcionando sin cambios.

¿Qué pasa si la API está caída durante un checkout? Con la arquitectura de arriba, nada visible para el cliente. La capa de pricing lee de un transient que rellena un job en background; si el último refresh falló, la tasa anterior sigue cacheada y la opción persistente guarda un fallback de 24 horas. El checkout sigue a la última tasa conocida válida.

¿Cómo gestiono devoluciones entre divisas? Guarda la tasa usada al momento del pedido en metadatos del order. Al devolver, calcula el importe con esa tasa, no la de hoy, para que el cliente quede compensado en la divisa con la que pagó. Tu sistema contable puede reconciliar el P&L de FX por separado.

Cierre

Una integración sólida de API multidivisa para WooCommerce no va realmente de la llamada a la API — va de las cuatro capas que la rodean: caché, programación, fallbacks y tests. Hazlas bien y tu tienda mantendrá precios honestos en 50+ divisas sin una sola alerta nocturna.

¿Listo para integrar tipos de cambio en tiempo real en tu tienda WooCommerce? Consigue tu clave gratuita de Finexly — sin tarjeta de crédito. Empieza con 1.000 peticiones gratis al mes, mete los snippets de arriba en un mu-plugin y tendrás precios multidivisa en producción antes de comer. Cuando superes el plan gratis, los planes de precios escalan linealmente con tu tienda, no exponencialmente.

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 →