WordPress IP Geolocation: Detect Country and Localize Content
WordPress IP geolocation options
You have three paths to add geo detection in WordPress:
- Plugin (no code) — install wp-apogeo for shortcode-based geo content
- PHP in functions.php — call ApogeoAPI from server-side PHP
- JavaScript in the browser — detect country after page load
Option 1: wp-apogeo plugin (recommended)
The free wp-apogeo plugin wraps ApogeoAPI with WordPress transient caching (4h TTL) and exposes four shortcodes:
// In any post, page, or widget:
[apogeo_visitor_country] // outputs "Germany" for DE visitors
[apogeo_country iso="US"] // outputs "United States" (static)
[apogeo_exchange_rate currency="EUR"] // outputs current EUR/USD rate
[apogeo_country_selector] // renders a <select> with all 250 countries
Install manually by uploading the plugin folder to wp-content/plugins/, then activate in the WordPress admin. Set your API key under Settings → ApogeoAPI.
Option 2: plain PHP in functions.php
Add a geo helper to your theme's functions.php:
<?php
// functions.php
function apogeo_get_visitor_geo(): array {
static $cache = null;
if ( $cache !== null ) return $cache;
$api_key = defined( 'APOGEO_API_KEY' ) ? APOGEO_API_KEY : get_option( 'apogeo_api_key', '' );
$transient_key = 'apogeo_geo_' . md5( $_SERVER['REMOTE_ADDR'] ?? '' );
$cached = get_transient( $transient_key );
if ( $cached !== false ) { $cache = $cached; return $cache; }
$url = 'https://api.apogeoapi.com/v1/ip/me';
$response = wp_remote_get( $url, [
'headers' => [ 'X-API-Key' => $api_key ],
'timeout' => 3,
'blocking' => true,
]);
if ( is_wp_error( $response ) ) {
$cache = [ 'country_code' => 'US', 'country_name' => 'United States' ];
} else {
$data = json_decode( wp_remote_retrieve_body( $response ), true );
$cache = $data ?? [ 'country_code' => 'US', 'country_name' => 'United States' ];
set_transient( $transient_key, $cache, 4 * HOUR_IN_SECONDS );
}
return $cache;
}
// Usage in any PHP template:
// $geo = apogeo_get_visitor_geo();
// $geo['country_code'] // "DE"
// $geo['country_name'] // "Germany"
// $geo['currency'] // "EUR"
Showing different content by country
Use the helper in any template or shortcode to conditionally render content:
// In a template file (e.g., page.php or block-template):
<?php
$geo = apogeo_get_visitor_geo();
$cc = $geo['country_code'] ?? 'US';
$eu = [ 'DE','FR','IT','ES','NL','BE','AT','PT','SE','FI','DK','PL','CZ','RO','HU','BG','HR','SK','SI','EE','LV','LT','CY','LU','MT' ];
$is_eu = in_array( $cc, $eu, true );
?>
<?php if ( $is_eu ) : ?>
<p>🇪🇺 VAT included in prices for EU customers.
<a href="/privacy-gdpr">Privacy information (GDPR)</a></p>
<?php elseif ( $cc === 'US' ) : ?>
<p>🇺🇸 Free shipping to the continental US on orders over $50.</p>
<?php else : ?>
<p>🌍 Shipping available to <?php echo esc_html( $geo['country_name'] ?? 'your region' ); ?>.</p>
<?php endif; ?>
WooCommerce: display price in local currency
WooCommerce supports multi-currency through plugins (WooCommerce Payments, WOOCS, CURCY). For a simple display-only conversion without a plugin, use ApogeoAPI's exchange rates:
function apogeo_localized_price( float $usd_price ): string {
$geo = apogeo_get_visitor_geo();
$currency = $geo['currency'] ?? 'USD';
if ( $currency === 'USD' ) {
return '$' . number_format( $usd_price, 2 );
}
// Fetch rate (cached separately for 4 h)
$rate_key = 'apogeo_rate_' . $currency;
$rate = get_transient( $rate_key );
if ( $rate === false ) {
$api_key = defined( 'APOGEO_API_KEY' ) ? APOGEO_API_KEY : get_option( 'apogeo_api_key', '' );
$resp = wp_remote_get(
"https://api.apogeoapi.com/v1/exchange-rates/USD",
[ 'headers' => [ 'X-API-Key' => $api_key ], 'timeout' => 3 ]
);
if ( ! is_wp_error( $resp ) ) {
$data = json_decode( wp_remote_retrieve_body( $resp ), true );
$rate = $data['rates'][ $currency ] ?? 1.0;
set_transient( $rate_key, $rate, 4 * HOUR_IN_SECONDS );
}
}
$local_price = round( $usd_price * (float) $rate, 2 );
return $currency . ' ' . number_format( $local_price, 2 );
}
// Usage:
echo apogeo_localized_price( 29.00 ); // "EUR 26.80" for German visitors
Note: For actual checkout in local currency you need a proper multi-currency checkout extension. This pattern is for display-only localisation.
Option 3: JavaScript (browser-side)
If you can't modify PHP files (e.g., page builder sites), enqueue a small script:
// functions.php — enqueue the geo detection script
add_action( 'wp_enqueue_scripts', function () {
wp_enqueue_script(
'apogeo-detect',
plugin_dir_url( __FILE__ ) . 'js/geo-detect.js',
[],
'1.0.0',
true // load in footer
);
wp_localize_script( 'apogeo-detect', 'ApogeoConfig', [
'apiKey' => defined( 'APOGEO_API_KEY' ) ? APOGEO_API_KEY : get_option( 'apogeo_api_key', '' ),
]);
});
// js/geo-detect.js
(async () => {
const KEY = 'apogeo_geo';
let geo = null;
try {
const raw = sessionStorage.getItem(KEY);
geo = raw ? JSON.parse(raw) : null;
} catch (_) {}
if (!geo) {
try {
const res = await fetch('https://api.apogeoapi.com/v1/ip/me', {
headers: { 'X-API-Key': ApogeoConfig.apiKey }
});
geo = await res.json();
sessionStorage.setItem(KEY, JSON.stringify(geo));
} catch (_) { return; }
}
// Show/hide elements with data-geo attributes
document.querySelectorAll('[data-geo-show]').forEach(el => {
const countries = el.dataset.geoShow.split(',');
el.style.display = countries.includes(geo.country_code) ? '' : 'none';
});
document.querySelectorAll('[data-geo-hide]').forEach(el => {
const countries = el.dataset.geoHide.split(',');
el.style.display = countries.includes(geo.country_code) ? 'none' : '';
});
})();
Then in any page builder you can add data-geo-show="DE,AT,CH" to show a block only to DACH visitors, or data-geo-hide="US" to hide something from US users.
Performance considerations
- Transient caching: the PHP examples cache per-IP for 4 hours — a single cached API call serves all page views from that visitor for 4 hours.
- Subnet caching: group IPs by first three octets (
1.2.3) to share one cache entry across a /24 subnet (~250 users). Reduces API calls ~250×. - Fallback defaults: always provide a default country code (
'US') if the API call fails, so your site works even during a network hiccup. - Free tier math: 1,000 req/month ÷ 30 days = ~33 unique IPs/day before caching. With /24 subnet caching you cover ~8,250 unique visitors/day on the free plan.
Try ApogeoAPI free
1,000 requests/month forever. 14-day full-access trial. No credit card.
Get your free API key