# Gate consent and defaults by country: is_eu, timezone and currency from an IP

Show the consent banner where the law requires it, pick a currency and clock the visitor recognises, and know where IP geolocation stops being reliable.

Date: 2026-09-04

The first paint of a page has to make three decisions before the visitor has told you anything: whether to ask for consent before setting non-essential cookies, which currency to quote, and what clock to show. An IP lookup answers all three well enough for a default, and badly enough that you must let the visitor override every one of them. This guide covers what to read, what to build on it, and where the answer genuinely stops.

## Consent: is_eu is a floor, not a border

The GDPR applies to the processing of personal data of people who are in the Union, whether or not they are citizens or residents, and to controllers established there regardless of where the processing happens (Article 3, Regulation (EU) 2016/679, https://eur-lex.europa.eu/eli/reg/2016/679/oj, checked 2026-09-04). The consent requirement for storing information on a visitor's device comes from Article 5(3) of Directive 2002/58/EC, the ePrivacy Directive (https://eur-lex.europa.eu/eli/dir/2002/58/oj), as transposed by each member state. Neither text says "show a banner to EU IP addresses". Both say the rules follow the person and the establishment.

What an IP lookup gives you is a reasonable proxy for "in the Union right now". The response carries `country` as an ISO code and `is_eu` as a boolean for the 27 member states, so the gate is one line:

```js
if (data.is_eu) showConsentBanner();
```

Three consequences follow, and each is a design decision rather than a lookup detail.

First, `is_eu` is false for the United Kingdom, Switzerland, Norway and Iceland, and for the EEA members among those the GDPR applies through the EEA agreement while the UK applies its own GDPR. Your policy should probably treat the EEA and the UK like the Union, which means checking `country` against your own list rather than relying on `is_eu` alone. The boolean is the floor.

Second, an EU resident on holiday in the United States is still protected, and your lookup will say `country: "US"`. If you serve EU customers at all, the cheapest safe policy is to ask for consent everywhere and use the lookup only to pick the default state of the toggles. Several jurisdictions outside the Union also require consent for tracking; a global banner sidesteps the question of keeping a country list current.

Third, `is_eu` is null when the country is unknown. Reserved addresses, the rare unlocated block, and lookups that could not resolve a country all answer null. Treat null as "ask".

## Currency and locale: a default the visitor can change

`currency` is the ISO 4217 code for the located country, taken from ICU's region data, the same source your platform's number formatter uses. `calling_code` comes from the same libphonenumber data that validates phone numbers. Both are country-level facts and are exactly as reliable as the country, which on the database used here is dependable at country level and approximate below it.

Use them for the first quote and the phone field's default prefix, and let the visitor switch. A German visitor sees euros; a German visitor connecting through a company VPN that exits in Ireland also sees euros, because Ireland is in the euro area, but one whose VPN exits in the United States sees dollars until they change it. That is why the location answer carries `location_source`: when a VPN provider's own server list places the address, the value is `operator` and the country is the exit's country, not the person's. If `is_vpn` or `is_relay` is true, prefer the visitor's stored preference or their browser language over the lookup.

Language is not in the response on purpose. Belgium, Switzerland, Canada and India each have several official languages, and the browser already tells you which one the visitor reads in `Accept-Language`. Use that header for language and the lookup for currency.

## Time zones: only where the answer is settled

`timezone` is an IANA identifier such as Europe/Berlin, from the tz database maintained by IANA (https://www.iana.org/time-zones). It is filled in only when the country has a single zone, or when the located region is known to lie entirely in one zone. Germany, Japan and France answer a zone; a visitor located in California answers America/Los_Angeles; a visitor located in Texas answers null, because the state straddles Central and Mountain time and the city-level location is not precise enough to pick one. A null here is not a failure. It is the API declining to guess between two clocks that differ by an hour.

Two lines cover both cases:

```js
const zone = data.timezone ?? Intl.DateTimeFormat().resolvedOptions().timeZone;
```

The browser knows its own zone precisely; the lookup is for the server side, where you have no browser: scheduling a digest email, rendering a receipt, choosing a support hours message. On the server, treat null as "use UTC and say so".

## Where the location comes from

The base answer is the DB-IP Lite database, refreshed monthly. Country-level accuracy from this data is dependable; city-level is approximate and varies by region. Where the network operator publishes the location itself, that declaration replaces the database answer: DigitalOcean, Linode and Vultr publish RFC 8805 geofeeds (https://www.rfc-editor.org/rfc/rfc8805), Apple publishes the egress ranges of iCloud Private Relay with their cities, and Mullvad, NordVPN and Private Internet Access publish their server lists. The response marks these with `location_source: "operator"`, and `network` tells you how wide the answer is: an answer for a /12 is a region, an answer for a /24 is a neighbourhood.

When the operator's country or city disagrees with the database, the region and the coordinates answer null rather than a contradicting point. Coordinates in the response are the centre of the located area, never a visitor's position; do not draw a pin with them.

## Reading the lookup on first paint

From a server you already have the address; from a page you do not, so the [browser endpoint](/docs/api/lookup-ip-public) looks up the visitor's own address when `ip` is omitted, authenticated by a publishable key locked to your domains:

```js
const response = await fetch('https://spaw.co/api/v1/ip/public', {
  method: 'POST',
  headers: { 'Content-Type': 'application/json' },
  body: JSON.stringify({ key: 'pk_live_…' }),
});
const { data } = await response.json();

document.documentElement.dataset.consent = data.is_eu === false ? 'optional' : 'required';
if (data.currency) setDefaultCurrency(data.currency);
```

Cache the answer in session storage. The address does not change between pages, and a repeat of the same address inside seven days is free anyway. From a server, `GET /api/v1/ip/me` ([reference](/docs/api/get-my-ip)) answers for the address the call came from, which is the quickest way to confirm what your own egress looks like to the outside world.

## What is kept

An IP address is personal data. Nothing about an IP lookup is stored on the service: no history row, no log line with the address. The only trace is a keyed seven-day marker that makes a repeat free, and `privacy: true` skips even that. Every signal is a local database or a compiled feed, so the address never reaches a third-party processor unless you opt into `abuse_contact`, which queries the Internet registry's public RDAP service. The [security page](/security) states the same facts with the rest of the data handling, and the [IP intelligence guide](/docs/ip-intelligence) documents every field used above.

## What to do next

- Read the [IP intelligence guide](/docs/ip-intelligence) for the full field list and the operator-location rules.
- Put the [browser endpoint](/docs/api/lookup-ip-public) behind a publishable key with a daily cap before it goes on a public page.
- Use the same lookup to keep scripts out of your forms with [block datacenter and bot signups with one IP lookup](/guides/block-datacenter-and-bot-signups-with-an-ip-lookup).

Reference: https://spaw.co/guides/gate-consent-and-defaults-by-country-from-an-ip
