# Country and currency from a visitor's IP | host.sh

> Pick a default country, currency and calling code from a visitor's IP address: the lookup fields, how far to trust them, and code that falls back safely.

A visitor's IP address tells you, most of the time, which country they are connecting from. That is enough to start a shop in the right currency, preselect a country in a form, or suggest a language, as long as you treat the answer as a first guess. This guide explains the country fields of a host.sh lookup, where country-level geolocation goes wrong, and how to use it as a default the visitor can change. The JavaScript, Python and PHP examples set a timeout, check the answer, and fall back to a fixed default when anything fails.

## The fields

A [lookup](https://www.host.sh/docs/lookup/index.md) answers with a `geo.country` object. Ask for only the parts you need with `fields=` (dotted paths, comma-separated):

```json
{"geo":{"country":{"code":"PT","is_eu":true,"calling_code":"+351","currency":{"code":"EUR"}}},"connection":{"security":{"is_vpn":false}}}
```

| Field | What it is |
|---|---|
| `geo.country.code` | The country as an [ISO 3166-1](https://www.iso.org/iso-3166-country-codes.html) alpha-2 code, such as `PT` or `US`; `null` when unknown |
| `geo.country.name` | The country's name, in English unless you ask for another language with `lang=` |
| `geo.country.currency.code` | The country's currency as an [ISO 4217](https://www.iso.org/iso-4217-currency-codes.html) code, such as `EUR`; `currency` also has `name` and `symbol`, and is `null` when unknown |
| `geo.country.calling_code` | The international calling code of the country, from the [ITU-T E.164](https://www.itu.int/rec/T-REC-E.164/) numbering plan, with a leading `+`, such as `+351` |
| `geo.country.is_eu` | Whether the country is a member of the European Union |
| `connection.security.is_vpn` | Whether the address is a known VPN exit; the [response fields](https://www.host.sh/docs/fields/index.md#connection-security-a-yes-no-summary) page has the other flags |

Two things to keep in mind about the codes. A calling code is not unique to one country: `+1` is shared by the United States, Canada and many Caribbean countries, so use it to prefill a phone field, not to work out the country. And some countries use more than one currency, while the field names one, so let the visitor pick another.

For the visitor's own address, a lookup needs no token, so a web page can ask from the browser. To look up an address on your server, for example the address of a request, you need a [token](https://www.host.sh/docs/index.md#authentication).

## How accurate country detection is

Country is the most dependable part of IP geolocation: addresses are handed out to networks, and most networks serve customers in one country. But an IP address shows where a connection comes out onto the internet, not where the person is or where they live, and a few common cases break the link:

- **VPNs and proxies.** The address is the VPN's exit, in whatever country the user picked. `connection.security.is_vpn` flags known VPN exits; when it is `true`, the country says where the VPN is, not the visitor. `is_proxy` and `is_tor` work the same way.
- **Mobile networks.** A phone's traffic can leave the carrier's network through a gateway that serves a whole region, and a phone roaming abroad often still comes out at its home carrier. The country can then be the carrier's, not the one the phone is in.
- **Satellite internet.** The address can belong to a ground station in a neighbouring country.
- **Corporate networks and cloud hosts.** A company may send all its offices' traffic out through one country, and a server in a data centre has the data centre's country.
- **Registration is not location.** The registry records of a network name the organisation that holds it, which can be in another country than the people using it. Geolocation data looks at where an address is used, but a newly moved network can be wrong for a while.

None of these are rare, so design for a wrong answer: it should cost the visitor one click, not a blocked page.

## Use it as a default, never as a verdict

- **Pricing.** Show prices in the detected currency as a starting point, with a visible currency selector, and remember the visitor's choice so the lookup doesn't overrule it on the next visit. Charge in the currency the visitor saw and confirmed.
- **Language.** The country is a weak hint for language: many countries have several languages, and people travel and move. The browser's `Accept-Language` header says what the visitor reads; use the country only when that is missing.
- **Forms.** Preselect the country and the calling code, and leave both editable.
- **Rules and compliance.** Tax, export, licensing and age rules need what the law asks for, which is seldom an IP address alone. IP country can be one signal among several, for example next to a billing address, but it is not proof of where someone lives, and VPNs move it at will.
- **Blocking.** Don't refuse service on IP country alone without a way through for people it misjudges, such as a support contact or another check.

## In JavaScript, in the browser

The visitor's own country, with no token, straight from the page (the API allows cross-origin requests). It asks once per browser session and keeps the answer:

```js
const FALLBACK = { country: 'US', currency: 'USD' };
const KEY = 'hostsh-locale';
const COUNTRY = /^[A-Z]{2}$/, CURRENCY = /^[A-Z]{3}$/;
const isCode = (value, re) => typeof value === 'string' && re.test(value);

async function visitorLocale() {
  try {
    const saved = JSON.parse(sessionStorage.getItem(KEY));
    if (isCode(saved?.country, COUNTRY) && isCode(saved?.currency, CURRENCY)) return saved;
  } catch {
    // no storage here (or not a browser), or not what this code stored: ask the API
  }
  try {
    const res = await fetch('https://api.host.sh/v2/lookup/?fields=geo.country.code,geo.country.currency.code,connection.security.is_vpn',
      { signal: AbortSignal.timeout(3000) });
    if (!res.ok) return FALLBACK;
    const data = await res.json();
    const country = data.geo?.country?.code;
    const currency = data.geo?.country?.currency?.code;
    if (!isCode(country, COUNTRY)) return FALLBACK;
    const locale = {
      country,
      currency: isCode(currency, CURRENCY) ? currency : FALLBACK.currency,
      // the country of a VPN exit is a weaker guess: still a default, never a rule
      vpn: data.connection?.security?.is_vpn === true,
    };
    try { sessionStorage.setItem(KEY, JSON.stringify(locale)); } catch {}
    return locale;
  } catch {
    return FALLBACK; // network error, timeout or a body that isn't JSON
  }
}

console.log(await visitorLocale());
```

Tokenless lookups are limited to 10 a minute per address ([rate limits](https://www.host.sh/docs/rate-limits/index.md)). One request per session stays well under that for most visitors, but people behind one shared address (an office, a school, a mobile carrier's NAT) share the limit too; over it the API answers `429`, and the code above then uses the fallback.

## On your server, in Python

On the server you look up the address of the request, which needs a token. Take the client address the way the [client IP guide](https://www.host.sh/guides/client-ip-behind-proxy/index.md) explains when you run behind a proxy or a CDN. Only the standard library:

```python
import json
import re
import urllib.parse
import urllib.request


class NoRedirect(urllib.request.HTTPRedirectHandler):
    def redirect_request(self, *args):
        return None  # a redirect would take the token along: fail instead


OPENER = urllib.request.build_opener(NoRedirect)

FALLBACK = {"country": "US", "currency": "USD"}


def visitor_locale(ip, token):
    """A default country and currency for this visitor; let them change it."""
    try:
        query = urllib.parse.urlencode({"ip": ip, "fields": "geo.country.code,geo.country.currency.code"})
        req = urllib.request.Request("https://api.host.sh/v2/lookup/?" + query, headers={"Authorization": "Bearer " + token})
        with OPENER.open(req, timeout=3) as res:  # an HTTP error or a redirect raises
            country = json.load(res)["geo"]["country"]
    except Exception:
        return FALLBACK
    if not isinstance(country, dict):
        return FALLBACK
    code = country.get("code")
    currency = country.get("currency")
    currency = currency.get("code") if isinstance(currency, dict) else None
    if not isinstance(code, str) or not re.fullmatch(r"[A-Z]{2}", code):
        return FALLBACK
    if not isinstance(currency, str) or not re.fullmatch(r"[A-Z]{3}", currency):
        currency = FALLBACK["currency"]
    return {"country": code, "currency": currency}


print(visitor_locale("8.8.8.8", "YOUR_TOKEN"))  # the request's client address in a real app
```

## On your server, in PHP

```php
<?php
const FALLBACK = ['country' => 'US', 'currency' => 'USD'];

// a default country and currency for this visitor; let them change it
function visitor_locale(string $ip, string $token): array
{
    $url = 'https://api.host.sh/v2/lookup/?' . http_build_query(['ip' => $ip, 'fields' => 'geo.country.code,geo.country.currency.code']);
    $ctx = stream_context_create(['http' => ['timeout' => 3, 'header' => 'Authorization: Bearer ' . $token, 'follow_location' => 0]]);
    $fp = @fopen($url, 'r', false, $ctx); // false on a network or HTTP error
    if ($fp === false) {
        return FALLBACK;
    }
    $body = stream_get_contents($fp);
    $meta = stream_get_meta_data($fp);
    fclose($fp);
    // only a 200 (redirects aren't followed: they would take the token along), and a timeout or a
    // connection that closed early leaves a short body
    $headers = $meta['wrapper_data'] ?? [];
    if (!preg_match('#^HTTP/\S+ 200\b#', $headers[0] ?? '')) {
        return FALLBACK;
    }
    $length = null;
    foreach ($headers as $header) {
        if (preg_match('/^Content-Length:\s*(\d+)\s*\z/i', $header, $m)) {
            $length = (int) $m[1];
        }
    }
    if ($body === false || $meta['timed_out'] || ($length !== null && strlen($body) !== $length)) {
        return FALLBACK;
    }
    $data = json_decode($body, true);
    $country = is_array($data) ? ($data['geo']['country'] ?? null) : null;
    $code = is_array($country) ? ($country['code'] ?? null) : null;
    if (!is_string($code) || !preg_match('/^[A-Z]{2}\z/', $code)) {
        return FALLBACK;
    }
    $currency = is_array($country['currency'] ?? null) ? ($country['currency']['code'] ?? null) : null;
    if (!is_string($currency) || !preg_match('/^[A-Z]{3}\z/', $currency)) {
        $currency = FALLBACK['currency'];
    }
    return ['country' => $code, 'currency' => $currency];
}

// REMOTE_ADDR is the client in a web request (behind a proxy, see the client IP guide)
$locale = visitor_locale($_SERVER['REMOTE_ADDR'] ?? '8.8.8.8', 'YOUR_TOKEN');
echo $locale['country'], ' ', $locale['currency'], "\n";
```

## From a terminal

`curl host.sh` answers single fields of your own address as plain text, with no token:

```sh
country=$(curl -sS --fail-with-body --max-time 5 https://host.sh/country_code) || country=''
case "$country" in [A-Z][A-Z]) ;; *) country=US;; esac
echo "Country: $country"
```

`host.sh/currency` and `host.sh/calling_code` work the same way; the [curl page](https://www.host.sh/curl/index.md) lists every field.

## FAQ

### How do I get a visitor's country from their IP address?

Look the address up and read `geo.country.code`, an ISO 3166-1 alpha-2 code. In the browser the visitor's own address needs no token; on a server, send the request's client address with a token ([Lookup](https://www.host.sh/docs/lookup/index.md)).

### How accurate is IP-based country detection?

It is right for most visitors, but it shows where a connection comes out onto the internet. VPNs, mobile carriers routing through another country, satellite links and corporate networks all move it, so use it as a default the visitor can change. `connection.security.is_vpn` flags known VPN exits ([response fields](https://www.host.sh/docs/fields/index.md)).

### Which currency code does the lookup give?

`geo.country.currency.code` is the country's currency as an [ISO 4217](https://www.iso.org/iso-4217-currency-codes.html) code, such as EUR or USD. Where a country uses more than one currency the field names one, so offer a currency selector.

### Can I use the calling code to find the country?

No. Calling codes from the [ITU-T E.164](https://www.itu.int/rec/T-REC-E.164/) plan are shared: +1 covers the United States, Canada and many Caribbean countries. Use `geo.country.calling_code` to prefill a phone field and `geo.country.code` for the country.

### Should I block countries by IP address?

Not on the IP country alone. It is wrong for VPN users, travellers and some mobile and corporate networks, so a hard block needs a fallback such as a support contact or another check. For rules that the law sets, use the evidence the law asks for.
