MaxMind

Look up a visitor's country from their IP address using MaxMind's GeoLite2 service, so XPAC User Targeting can vary popups and forms by country.

MaxMind answers one question for the rest of XPAC: which country is this visitor in? It has no screens of its own beyond a settings tab, shows nothing on your site, and adds no blocks. What it does is make a country available to XPAC User Targeting, which is what lets a popup open only for visitors in Canada, or a form show an extra field only to visitors in the EU.

The country comes from the visitor's IP address, checked against MaxMind's GeoLite2 service. You need a free MaxMind account for that.

User Targeting has to be active

The plugin does nothing on its own. Without XPAC User Targeting it registers no country lookup at all and shows an admin notice asking for it instead. User Targeting in turn only does something useful when Popups or Forms is active, since those are what act on the country.

Where to find it

XPAC → Integrations, on the Max Mind tab. Two boxes, both required.

Setting it up

Create a free account at maxmind.com. MaxMind stopped allowing anonymous access years ago, so there is no way to skip this.

In your MaxMind account, note the Account ID shown on your account page, then generate a License key under Manage License Keys. MaxMind's own instructions for generating a key are linked from the settings screen.

Paste both into XPAC → Integrations → Max Mind and save. Both boxes must be filled — with either one empty, nothing is switched on.

Edit a popup or a form. A User Country condition now appears in the targeting rules, listing every country. Before the keys were saved that condition was not offered at all.

SettingWhat it is
Account IDThe numeric account ID from your MaxMind account page.
License keyA key you generate in your MaxMind account. Not your password.

Nothing checks that your keys work

The keys are not tested when you save them, and there is no "test connection" button, no status indicator and no error message anywhere. A typo in either box produces exactly the same result as no country data at all: the User Country condition still appears in the editor and still lets you pick a country, and it then matches nobody — with one exception, an is not rule inside a form, which matches everybody instead. See below. If your country rules are not behaving, re-check the keys first — nothing else will tell you.

If you already use WooCommerce

WooCommerce has its own MaxMind integration, and if it is set up, this plugin steps aside and uses WooCommerce's answer instead of asking MaxMind itself.

That happens automatically whenever WooCommerce is active and has a MaxMind license key saved under its own settings. It is usually what you want — one lookup, one account, one place to manage it.

Your XPAC keys are then ignored, silently

When WooCommerce is in charge, the Account ID and License key you typed into XPAC → Integrations → Max Mind are still saved and still shown back to you, but never used. Nothing on either screen mentions the other. The reverse trap is worse: remove the key from WooCommerce's settings, or deactivate WooCommerce, and country lookups quietly switch over to your XPAC keys — so if those are blank or wrong, every country rule on the site stops matching, with no notice.

What you get back

A single answer: a two-letter country code such as GB or DE, or nothing at all when the visitor could not be placed.

There is no region, city, postcode or coordinate — only the country. Rules can compare it two ways, is and is not.

On a form, a failed lookup makes “is not” rules match everyone

An unknown country — a bad key, a MaxMind outage, a visitor whose address is not in the database — is handled differently by the two places you can use a country rule.

  • In a popup, neither direction matches. is Germany shows to nobody, and so does is not Germany. Both fail closed, and you will notice.
  • In a form section behind Conditional Logic, is not Germany still matches. The country is known to be empty rather than unavailable, and an empty country is not Germany. A section meant for everyone except Germany is shown to everyone, Germany included, and nothing says so.

Deactivating MaxMind altogether is a different case again: with no provider registered at all the country condition cannot be resolved, and both places fail closed.

If you are excluding a country for a reason that matters, do not rely on this rule alone.

Country rules are not a security control

The country comes from the address your server sees, and that is a guess about a person, not proof of where they are. A VPN, a proxy or a corporate network is enough to change it, and MaxMind's own answer is an estimate.

Client-supplied X-Real-IP and X-Forwarded-For headers — the way a visitor could once simply name their own address — are ignored unless the site has declared a reverse proxy in front of it with the XPAC_TRUST_PROXY constant. If you do set that constant, make sure nothing can reach the origin server directly, or naming your own country becomes one extra line in a request again.

Either way this is fine for the things it is meant for — showing a regional offer, pre-selecting a country in a form, opening a different popup. It is not fine for anything with consequences: content licensing, region-locked pricing, age or legal restrictions, or anything you would have to defend.

Speed, and what it costs

The first visitor from a given address on a page that has a country rule waits while your server asks MaxMind. The call is allowed up to three seconds before it gives up. The answer is then remembered for twelve hours, so that wait happens once per address rather than once per page view — and it is remembered whether or not your host runs a persistent object cache, because the answer is stored as a WordPress transient as well as in the object cache.

Country rules and full-page caching pull in opposite directions

The visitor's country is written into the page itself, which is the one thing a full-page cache cannot cope with: store one visitor's page, serve it to the next, and everybody gets the first visitor's country.

XPAC now tells caches not to store those pages. On any page where a country was actually resolved it sets the DONOTCACHEPAGE constant that WordPress caching plugins read, and sends no-cache headers that a reverse proxy or CDN reads. Most caches honour one or the other.

"Resolved" is wider than "used". Nothing asks whether a rule needs the country before looking it up: a page renders a popup, or a page renders any form at all, and the country is resolved and printed. With this plugin configured and a form in your footer, that is every page with a footer, and all of them are now excluded from caching. On a site that leans on full-page caching this is a real change in how much of it is cached — measure before and after.

Two more things follow. A CDN told to cache everything and ignore what your origin says will still serve the wrong country, so exclude those pages there yourself. And if the reach above is wider than you want, the only lever is this plugin: with no keys configured no provider is registered, nothing is resolved, and nothing is excluded.

The free GeoLite2 service also has a daily request limit set by MaxMind. A busy site with many distinct visitors can reach it even with the caching above, since each new address is one request. Once you do, lookups start failing, which under the callouts above means your "is" rules match nobody and, in a form, your "is not" rules match everybody — with nothing anywhere to tell you the limit was the cause. Check the usage figures in your MaxMind account if country rules start behaving oddly.

Privacy

An IP address is personal data in the UK and EU, so this is worth being deliberate about.

What happensDetail
The visitor's IP address is sent to MaxMindMaxMind is a third party, based in the United States. This happens for visitors on pages that carry a country rule.
The resulting country is cached on your serverHeld for twelve hours against the address it came from, or ten minutes if the lookup failed. It is stored as a WordPress transient, so on most sites that means a row in your database, keyed by a hash of the address and removed when it expires.
The country is written into the pageVisible in the page source to that visitor.
Nothing else is storedNo cookie is set, no user profile field is written, and no record is kept once the cache expires.

If your site has a privacy policy, it should mention that visitor IP addresses are shared with MaxMind for the purpose of determining country. There is no setting to exempt logged-in users or particular pages — if the plugin is configured, the lookup happens.

No database is downloaded to your site

Some geolocation tools download a large database file into your uploads folder and refresh it on a schedule, which brings its own housekeeping. This one does not. It asks MaxMind each time and caches the answer, so there is no file to protect, no update job to monitor and no database to go stale. The exception is when WooCommerce is doing the lookup instead — that one does keep a downloaded database, and WooCommerce manages it.

For developers

The plugin registers a userCountry data provider on User Targeting and does nothing else. It fires no hooks of its own, registers no blocks, adds no REST routes and creates no database tables — the entire package is two PHP files.

Settings are stored in the shared xpac option under the keys max_mind_account_id and max_mind_license_key, contributed as a max_mind page (priority 400) of the xpac settings module. The module is registered without show_in_rest, so neither key is exposed over the REST API. Both are rendered into the settings screen's data-values attribute along with every other value in the module — esc_attr-escaped and behind manage_options, and a gap in the shared settings framework (there is no masked field type) rather than something specific to this package.

Registration happens on init at priority 101, and takes one of two branches:

ConditionProvider callbackSource
\WooCommerce exists and woocommerce_maxmind_geolocation_settings[license_key] is non-emptygetCountryCodeThroughWooCommerce()WC_Geolocation::geolocate_ip()['country'] — WooCommerce's downloaded GeoLite2 database
Otherwise, both XPAC keys non-emptygetCountryCodeStandalone()GET https://geolite.info/geoip/v2.1/country/{ip}, HTTP Basic auth, 3s timeout
Neithernone registeredThe userCountry provider stays null and every consumer short-circuits

The WooCommerce branch returns before the XPAC settings are read, so $accountId and $licenseKey are left uninitialised on that path. Harmless today only because requestCountryCode() is unreachable there.

The IP address comes from the framework's own userIpAddress provider, reached through the xpac_data_providers filter. REMOTE_ADDR is the only source read by default: X-Real-IP and then the first entry of X-Forwarded-For are consulted first, but only when the site has declared that a reverse proxy sits in front of it, with the XPAC_TRUST_PROXY constant or the xpac_trust_proxy_headers filter. All three go through rest_is_ip_address, so a header carrying anything that is not an address is discarded rather than interpolated into the request URL.

When none of them yields an address — WP-CLI, cron, or an origin that never sees REMOTE_ADDR — the lookup is abandoned and '' is returned before the cache is touched. It used to fall through to the literal 'me', which is MaxMind's endpoint for "the address this request came from": the web server. The datacentre country that came back was cached under a single shared key and handed to every visitor for the next twelve hours.

Caching is a transient, xpac_maxmind_country_<md5(ip)>, read through a wp_cache_* entry in the xpac_maxmind group under the same key, 12 * HOUR_IN_SECONDS on success and 10 * MINUTE_IN_SECONDS on failure. The object cache is non-persistent unless a backend is installed, which is why the transient is there: without it the TTL only ever applied within a single request and every page view made a fresh blocking call.

Failure handling in requestCountryCode() returns '' for a WP_Error, for a body that does not decode, and for a decoded body with no country.iso_code. The HTTP status is never inspected, so a 401 (bad credentials), 402 (quota exhausted) and 404 (IP not in the database) are indistinguishable from each other and from success-with-no-country. Nothing is logged and no notice is raised.

Consumers call the provider through Bootstrap::instance()->getDataProvider('userCountry'):

ConsumerWhere the value goes
UserTargeting\PopupsInlined into window.xpacUserTargeting['---user-geo-country---'] in the page markup
UserTargeting\FormsInlined via xpac_forms_inline_js_data, plus a lazy callback on xpac_forms_provider_blocks_data

Front-end matching for popups is assets/packages/user-targeting/popups/front/rules/user-country.js, a case-insensitive === / !== against the rule's country, behind an '' === country guard that returns false for both directions — so a failed lookup fails closed there. Conditional Logic has no such guard: its evaluators compare the empty string, so != matches. See the callout above.

Package review notes, including the header-spoofing and page-cache findings, are in packages/MaxMind/ISSUES.md.

On this page