Most WooCommerce stores that sell internationally rely on automatic currency conversion — the store has one base price, and a plugin multiplies it by the current exchange rate to show a localized amount. This works for many stores, but it creates a problem when the business needs to control prices manually per market. A product priced at IDR 150,000 does not always convert cleanly to a price that makes commercial sense in USD or EUR, and exchange rate fluctuations can make pricing inconsistent over time.

This case study documents a custom WooCommerce multi-currency solution built for an e-commerce client selling digital products to customers across multiple countries. The requirement was simple: each product should have a manually set price per country, displayed automatically based on where the visitor is located, with no automatic conversion involved.

Project Overview

ProjectWooCommerce multi-currency with fixed prices per country
PlatformWordPress + WooCommerce
ClientE-commerce store (anonymous)
ApproachCustom code — no premium plugin required
Key featuresPer-product per-country pricing, GeoIP auto-detection, custom admin metabox, cart and checkout support

Why Not Use a Plugin?

The first step was evaluating existing plugins. The three most capable options — WOOCS Pro, Multi-Currency for WooCommerce by VillaTheme, and Aelia Currency Switcher — all support fixed prices per product per currency, but all require a paid license ranging from $32 to $79 per year. For a store with a modest product catalog and a clear set of target countries, paying for a full-featured plugin with dozens of options the client would never use was not the right fit.

The custom approach instead used WooCommerce’s own GeoIP class, a custom admin metabox for price input, and a set of WooCommerce filters to override pricing at every point in the purchase flow. No third-party plugin dependency, no recurring license cost, and full control over exactly how the logic works.

How It Works

When a visitor lands on the store, their IP address is passed to WooCommerce’s built-in WC_Geolocation class, which returns a country code. That country code is used to look up a custom price stored in the product’s post meta. If a price exists for that country, it replaces the default WooCommerce price at every display point — the shop listing, the single product page, and the cart and checkout totals. If no custom price is set for the detected country, the store falls back to the default WooCommerce price.

Each country price entry also stores a custom currency symbol. This symbol is used to override WooCommerce’s global currency display, so a visitor from Japan sees ¥ and a visitor from Indonesia sees Rp — even though the store’s base currency is set to something else in WooCommerce settings.

The Admin Interface

A custom metabox was added to the WooCommerce product edit screen. The metabox presents a searchable dropdown of all countries. When a country is selected, a new row appears with fields for the price and the currency symbol for that country. The store owner can add as many country rows as needed, and each row can be removed individually. On save, the data is stored as a serialized array in a single post meta key (_country_prices) against the product.

The metabox uses nonce verification and capability checks before saving, and all input is sanitized before storage. Countries that have already been added are disabled in the dropdown to prevent duplicates.

GeoIP Detection

Country detection uses WooCommerce’s own geolocation infrastructure rather than a third-party API:

function get_user_country_code() {
    if ( class_exists( 'WC_Geolocation' ) ) {
        $geo      = new WC_Geolocation();
        $ip       = $geo->get_ip_address();
        $location = $geo->geolocate_ip( $ip );
        return isset( $location['country'] ) ? strtoupper( $location['country'] ) : null;
    }
    return null;
}

This function is called at the point of price lookup rather than cached at page load, which means it works correctly even if WooCommerce’s own geolocation cache is involved. The result is not stored in a separate cookie — WooCommerce handles its own geolocation caching internally.

Price Override Filters

Three WooCommerce filters are hooked to apply the custom country price wherever WooCommerce reads product pricing:

add_filter( 'woocommerce_product_get_price', 'set_price_by_country', 20, 2 );
add_filter( 'woocommerce_product_get_regular_price', 'set_price_by_country', 20, 2 );
add_filter( 'woocommerce_product_get_sale_price', 'set_price_by_country', 20, 2 );

The filter callback checks whether the current context is admin, cart, or checkout before applying the override on single product pages and shop listings. Cart and checkout totals are handled separately via the woocommerce_before_calculate_totals action, which iterates over all cart items and sets the price directly on each product object before WooCommerce calculates the order total.

Variable products are excluded from the per-country price override at the product level — variable product pricing is managed through their variations, which would require a separate implementation if needed.

Currency Symbol Override

WooCommerce uses a single global currency symbol set in Settings › General. To display the correct symbol per visitor without changing the store’s base currency, the woocommerce_currency_symbol filter is hooked to return the symbol stored alongside the custom country price:

add_filter( 'woocommerce_currency_symbol', 'set_custom_currency_symbol', 10, 2 );

function set_custom_currency_symbol( $currency_symbol, $currency ) {
    if ( is_admin() ) return $currency_symbol;

    $product_id = is_product() ? get_the_ID() : null;

    if ( ! $product_id && ( is_cart() || is_checkout() ) ) {
        $cart = WC()->cart;
        if ( $cart && ! empty( $cart->get_cart() ) ) {
            foreach ( $cart->get_cart() as $item ) {
                $product_id = $item['product_id'];
                break;
            }
        }
    }

    if ( $product_id ) {
        $country     = get_user_country_code();
        $custom_data = $country ? get_custom_country_price( $product_id, $country ) : null;
        if ( $custom_data && ! empty( $custom_data['currency_symbol'] ) ) {
            return $custom_data['currency_symbol'];
        }
    }

    return $currency_symbol;
}

Limitations and Tradeoffs

This implementation has a few known limitations worth documenting. The currency symbol override in cart and checkout reads the symbol from the first product in the cart — if a cart contains products with prices in different currencies (because the visitor somehow has items from different pricing contexts), the symbol displayed may not match all items. For the client’s use case, this was not a concern because all products were priced consistently per country.

WooCommerce order records store prices in the store’s base currency, not in the displayed currency. This means order history in the admin will show the numeric value of what was charged but with the base currency symbol rather than the visitor’s local symbol. If order currency accuracy in the admin is important, a more complete solution would need to store the detected currency against each order at the time of purchase.

GeoIP detection accuracy depends on WooCommerce’s geolocation database being up to date, and VPN users will be detected based on their VPN exit location rather than their actual country. These are inherent constraints of any IP-based geolocation approach.

Result

The solution was deployed without any premium plugin. The store owner can now set a specific price and currency symbol for each country directly from the product edit screen. Visitors are shown the correct price for their location automatically on arrival, and that price carries through to the cart and checkout total. The implementation is entirely contained in custom code, making it easy to maintain or extend without being tied to a plugin’s update cycle or licensing terms.