---
title: "Show Prices in Local Currency by IP: Node.js Tutorial"
slug: "/tutorials/show-prices-in-local-currency-based-on-ip"
description: "Detect each visitor's currency from their IP, convert your base price, and format it with Intl.NumberFormat. Node.js code, fallbacks, and VPN handling."
---

# Show Prices in Local Currency Based on IP

By [Daud Ahmad](https://www.linkedin.com/in/daudahmad0303) Software Engineer

Posted on October 22, 2026 | 14 min read

![Show Prices in Local Currency Based on IP](https://blogs.ipgeolocation.io/blogs-site/content/images/2026/10/show-prices-in-local-currency-by-ip-1.png)

To show prices in local currency, look up the visitor's IP address on your server, read the ISO 4217 currency code from the response, convert your base price with an exchange rate, and format the result for the visitor's locale. IPGeolocation.io returns that code in its default response as `currency.code` next to the visitor's country and its languages.

Picture one pricing page and three visitors. A shopper in Berlin should see euros and one in Tokyo yen, while a third, on a VPN server in Dallas, gets dollars and needs a switcher that wins over detection. You'll build that page in Node.js with Express, [the IPGeolocation.io API](https://ipgeolocation.io/ip-location-api.html) for detection, and [CurrencyFreaks](https://currencyfreaks.com/) for exchange rates.

* * *

## TL;DR

To show prices in local currency in Node.js, install `ip-geolocation-api-javascript-sdk`, call `lookupIpGeolocation()` with the visitor's IP, and read `currency.code`. Convert with a cached exchange rate and format with `Intl.NumberFormat`.

*   Ask for three fields only: `location.country_code2`, `currency`, and `country_metadata.languages`.
*   A saved choice beats the detected currency, and the detected currency beats your base currency.
*   Cache the detected currency per IP for a day, and the rates until your provider updates them.
*   For visitors flagged as VPN or anonymous, keep the base currency and show the currency switcher.

![Flow from visitor request to IP lookup, exchange rate, and locale formatting, with saved choice and base-currency fallback](https://blogs.ipgeolocation.io/blogs-site/content/images/2026/10/ip-to-local-currency-price-flow.png)

* * *

## Decide what the local price promises

Showing a price in someone's currency and charging them in it are two different jobs, and "localized pricing" can mean either, so pick one before you write code.

For stores and small SaaS products, the IPGeolocation.io team's advice is to start with display only. Display prices in the visitor's local currency, keep checkout in your base currency (USD or EUR, say), label converted prices as estimates, and say which currency the card will be charged in. Add local-currency billing later, if your payment provider supports it. The code below converts for display only.

* * *

## Before you start

*   `node -v` prints 18 or later. The IPGeolocation.io JavaScript SDK and Express 5 both need it.
*   You have an IPGeolocation.io API key. The free plan includes 1,000 credits per day, and the currency and country metadata used here are part of it.
*   You have a CurrencyFreaks API key. Its [free Developer plan](https://currencyfreaks.com/pricing) allows 1,000 API calls a month, with rates updated daily against USD.
*   Both keys sit in environment variables, `IPGEO_API_KEY` and `CURRENCYFREAKS_API_KEY`, never in browser code. [Storing API keys in environment variables](https://ipgeolocation.io/tutorials/secure-api-key-before-production#store-your-api-key-in-environment-variables) covers the options.

Then create the project:

```bash
mkdir local-prices && cd local-prices
npm init -y
npm pkg set type=module
npm install ip-geolocation-api-javascript-sdk express cookie-parser
```

Each step explains one part of `server.js`; copy the complete file at the end of Step 5 to run it.

* * *

## Step 1: Read the visitor's IP on the server

Express gives you the visitor's IP as `req.ip`, but only once you tell it how many proxies sit in front of your app. Without that, `req.ip` is your load balancer's address:

```js
const app = express();
app.set("trust proxy", 1); // one proxy (your load balancer) in front; match your setup

// Inside the /pricing route: TEST_IP only replaces loopback addresses
const isLocal = ["::1", "127.0.0.1", "::ffff:127.0.0.1"].includes(req.ip);
const ip = isLocal && process.env.TEST_IP ? process.env.TEST_IP : req.ip;
```

With one trusted hop, Express takes the client address that your own proxy appended to `X-Forwarded-For` and ignores anything a client wrote further left. With no proxy in front, leave `trust proxy` off, or clients can pick their own IP. For Nginx, Cloudflare, or several layers, see [the trust model for forwarded headers](https://ipgeolocation.io/guides/get-real-client-ip-address#the-trust-model-who-set-the-header).

On your laptop, `req.ip` is `::1` or `127.0.0.1`, and the API answers a private address with a 423. Set `TEST_IP` to `213.133.116.44` (the web server behind hetzner.com, in Germany) or to your own public address, which [the what-is-my-ip API tutorial](https://ipgeolocation.io/tutorials/what-is-my-ip-api#get-your-public-ip-with-curl) helps you find. The route uses it only for requests from localhost.

* * *

## Step 2: Look up the visitor's currency

One call to the IPGeolocation.io SDK returns the currency together with the country and its languages. Request only those fields, and cache the answer per IP:

```js
import express from "express";
import cookieParser from "cookie-parser";
import {
  IpGeolocationClient,
  LockedError,
  RateLimitError,
  UnauthorizedError,
} from "ip-geolocation-api-javascript-sdk";

const BASE_CURRENCY = "USD"; // the currency you charge in
const DAY_MS = 24 * 60 * 60 * 1000;

const ipgeo = new IpGeolocationClient({ apiKey: process.env.IPGEO_API_KEY });

// Step 2: detect the visitor's currency, cached per IP for a day
const currencyCache = new Map();

async function lookupCurrency(ip) {
  const hit = currencyCache.get(ip);
  if (hit && hit.expires > Date.now()) return hit.value;

  let value = null;
  try {
    const { data } = await ipgeo.lookupIpGeolocation({
      ip,
      fields: ["location.country_code2", "currency", "country_metadata.languages"],
    });
    value = {
      currency: data.currency?.code || null, // can be empty for some locations
      country: data.location?.countryCode2 || null,
      languages: data.countryMetadata?.languages ?? [],
    };
  } catch (err) {
    if (err instanceof LockedError) {
      // 423: private or bogon IP, such as ::1 on localhost; nothing to detect
    } else if (err instanceof UnauthorizedError || err instanceof RateLimitError) {
      console.error(`IPGeolocation.io ${err.statusCode}: ${err.apiMessage ?? err.message}`);
    } else {
      console.error(`Currency lookup failed for ${ip}: ${err.message}`);
    }
  }

  // Cache failures briefly so a timeout doesn't pin a visitor to the base currency for a day
  currencyCache.set(ip, { value, expires: Date.now() + (value ? DAY_MS : 5 * 60 * 1000) });
  if (currencyCache.size > 50_000) currencyCache.delete(currencyCache.keys().next().value);
  return value;
}
```

Calling `lookupCurrency("213.133.116.44")` returns:

```text
{ currency: 'EUR', country: 'DE', languages: [ 'de' ] }
```

`currency.code` is the code the formatter needs. `location.country_code2` drives market rules, such as the Panama override below. `country_metadata.languages` lists the country's languages by prevalence and becomes the locale fallback in Step 4. Because the response carries the currency itself ([currency object reference](https://ipgeolocation.io/documentation/ip-location-api.html#currency-json-object-reference)), you don't maintain a country-to-currency table.

The team suggests caching a visitor's currency for a day to a week; the code uses a day. An empty currency, a 401 from a wrong key, or a 429 after the daily credits run out all return `null`, and the visitor sees your base currency.

Of the team's edge cases, the euro area needs nothing special: many countries, one code. Panama needs a choice: our lookup for the University of Panama's web server returns PAB (the balboa), while the US dollar circulates alongside it, so the complete file maps `PA` to USD in `COUNTRY_OVERRIDES`. Delete that entry to show balboas.

![IPGeolocation.io response for a German IP with currency EUR, languages de, and country DE, each mapped to its use](https://blogs.ipgeolocation.io/blogs-site/content/images/2026/10/ip-lookup-currency-response-fields.png)

* * *

## Step 3: Convert with exchange rates

Detection tells you which currency to show, not what your price is in it, and mixing the two up is one of the two mistakes IPGeolocation.io's team sees most. This step takes rates from [CurrencyFreaks' latest rates endpoint](https://currencyfreaks.com/documentation#Latest) and caches them:

```js
// Step 3: exchange rates from CurrencyFreaks, cached for the plan's update interval
let ratesCache = { rates: null, expires: 0 };

async function getRates() {
  if (ratesCache.expires > Date.now()) return ratesCache.rates;

  const url = new URL("https://api.currencyfreaks.com/v2.0/rates/latest");
  url.searchParams.set("apikey", process.env.CURRENCYFREAKS_API_KEY ?? "");
  try {
    const res = await fetch(url, { signal: AbortSignal.timeout(5000) });
    const body = await res.json();
    if (!res.ok) throw new Error(`HTTP ${res.status}: ${body.message ?? "no message"}`);
    const rates = { ...body.rates, [body.base]: "1" }; // every rate is against body.base (USD)
    ratesCache = { rates, expires: Date.now() + DAY_MS }; // free plan: daily updates
  } catch (err) {
    console.error(`Rate refresh failed: ${err.message}`);
    ratesCache.expires = Date.now() + 10 * 60 * 1000; // keep the last good rates, retry later
  }
  return ratesCache.rates; // null until the first successful refresh
}
```

The endpoint returns every rate as a string against USD, as in this trimmed example from CurrencyFreaks' documentation:

```json
{
  "date": "2026-08-25 13:26:00+00",
  "base": "USD",
  "rates": { "EUR": "0.9278605451274349", "GBP": "0.8172754173817152", "USD": "1.0" }
}
```

On the free plan, rates change once a day against USD, so a 24-hour cache costs about 30 calls a month against the 1,000 allowed. The code adds USD itself at a rate of 1, and Step 5 divides by your base currency's rate, so a EUR or GBP base works with the same feed. A failed refresh keeps the last good rates and retries in ten minutes. Before the first success, `getRates()` returns `null` and prices stay in your base currency.

* * *

## Step 4: Format the price for the visitor's locale

The currency decides the money; the locale decides how the number is written. Keep them separate; the IPGeolocation.io team advises: pass the ISO currency code to `Intl.NumberFormat`, and take the locale from the page's language when the visitor picked one, then from the browser's `Accept-Language` header, then from the country's languages. `Intl.NumberFormat` is how JavaScript formats currency, so there's no library to add.

```js
const PAGE_LOCALE = null; // set to the page's language (such as "de-DE") on localized pages

// Step 4: format with a locale the runtime supports
function usableLocale(tag) {
  if (!tag || tag === "*") return null;
  try {
    return Intl.NumberFormat.supportedLocalesOf(tag)[0] ?? null;
  } catch {
    return null; // malformed tag in Accept-Language
  }
}

function pickLocale(req, countryLanguages) {
  const candidates = [PAGE_LOCALE, ...req.acceptsLanguages(), ...countryLanguages, "en-US"];
  for (const tag of candidates) {
    const locale = usableLocale(tag);
    if (locale) return locale;
  }
  return "en-US";
}

function formatPrice(amount, currency, locale) {
  return new Intl.NumberFormat(locale, { style: "currency", currency }).format(amount);
}
```

Here's the same amount in the currencies our lookups returned, in locales a visitor might bring:

| `currency.code` | Locale | `formatPrice(1234.5, …)` | `currency.symbol` |
| --- | --- | --- | --- |
| EUR | de  | 1.234,50 € | €   |
| EUR | en-US | €1,234.50 | €   |
| JPY | ja  | ￥1,235 | ¥   |
| BRL | pt-BR | R$ 1.234,50 | R$  |

![Intl.NumberFormat output for EUR, JPY, and BRL in de, en-US, ja, and pt-BR, next to the API's currency symbols](https://blogs.ipgeolocation.io/blogs-site/content/images/2026/10/intl-numberformat-currency-locale-examples.webp)

The two euro rows use one currency and two layouts. The yen row has no decimals because the currency has none, and Japanese formatting prints the full-width ￥ where the API's symbol is the standard ¥. The `currency.symbol` field suits places where you write the text yourself: a switcher label such as "Prices in €", a compact badge, or a template in a system without `Intl`. A dollar sign alone is shared by several currencies, so pair it with the code in mixed lists.

* * *

## Step 5: Let the visitor's choice win

A visitor who picks a currency should keep it. The route resolves the currency in a fixed order: a saved choice from a cookie, then the detected currency, then your base currency. It skips any currency without a rate.

```js
const PLANS = [
  { name: "Starter", priceMinor: 900 }, // prices in cents of BASE_CURRENCY
  { name: "Pro", priceMinor: 4900 },
];
const SWITCHER = ["USD", "EUR", "GBP", "JPY", "INR", "BRL"];

// Step 5: a saved choice beats detection; no rate means the base currency
function resolveCurrency(saved, detected, rates) {
  for (const code of [saved, detected]) {
    if (!/^[A-Z]{3}$/.test(code ?? "")) continue;
    if (code === BASE_CURRENCY) return { currency: code, rate: 1 };
    // Cross rate, so a EUR or GBP base works with a USD-based feed
    const rate = Number(rates?.[code]) / Number(rates?.[BASE_CURRENCY]);
    if (Number.isFinite(rate) && rate > 0) return { currency: code, rate };
  }
  return { currency: BASE_CURRENCY, rate: 1 };
}
```

The `/pricing` route in the complete file below renders on the server, so prices never flicker after the page loads, and `POST /currency` saves the switcher's choice in a cookie for a year.

Load your keys into the shell and start the server:

```bash
export IPGEO_API_KEY="your_ipgeolocation_key"
export CURRENCYFREAKS_API_KEY="your_currencyfreaks_key"
TEST_IP=213.133.116.44 node server.js
```

Open `localhost:3000/pricing` in a browser set to German. Our run on October 22, 2026 printed this list (yours will follow the day's rate):

```text
Starter: 8,01 € (approx.)
Pro: 43,62 € (approx.)
Prices in EUR are estimates. You're billed in USD.
```

Pick GBP in the switcher: the redirect reloads the page, the cookie wins, and the same visitor sees pounds. If converted estimates look odd next to round prices, set fixed prices for your main markets. The route also sends `Cache-Control: private, no-store`, which keeps a CDN from serving one visitor's prices to the next.

* * *

### 1. The complete server.js

```js
// server.js: show prices in the visitor's local currency (Node.js 18+, Express 5)
import express from "express";
import cookieParser from "cookie-parser";
import {
  IpGeolocationClient,
  LockedError,
  RateLimitError,
  UnauthorizedError,
} from "ip-geolocation-api-javascript-sdk";

const BASE_CURRENCY = "USD"; // the currency you charge in
const PLANS = [
  { name: "Starter", priceMinor: 900 }, // prices in cents of BASE_CURRENCY
  { name: "Pro", priceMinor: 4900 },
];
const SWITCHER = ["USD", "EUR", "GBP", "JPY", "INR", "BRL"];
const COUNTRY_OVERRIDES = { PA: "USD" }; // optional: show dollars in Panama instead of balboas
const PAGE_LOCALE = null; // set to the page's language (such as "de-DE") on localized pages
const DAY_MS = 24 * 60 * 60 * 1000;

const ipgeo = new IpGeolocationClient({ apiKey: process.env.IPGEO_API_KEY });

// Step 2: detect the visitor's currency, cached per IP for a day
const currencyCache = new Map();

async function lookupCurrency(ip) {
  const hit = currencyCache.get(ip);
  if (hit && hit.expires > Date.now()) return hit.value;

  let value = null;
  try {
    const { data } = await ipgeo.lookupIpGeolocation({
      ip,
      fields: ["location.country_code2", "currency", "country_metadata.languages"],
    });
    value = {
      currency: data.currency?.code || null, // can be empty for some locations
      country: data.location?.countryCode2 || null,
      languages: data.countryMetadata?.languages ?? [],
    };
  } catch (err) {
    if (err instanceof LockedError) {
      // 423: private or bogon IP, such as ::1 on localhost; nothing to detect
    } else if (err instanceof UnauthorizedError || err instanceof RateLimitError) {
      console.error(`IPGeolocation.io ${err.statusCode}: ${err.apiMessage ?? err.message}`);
    } else {
      console.error(`Currency lookup failed for ${ip}: ${err.message}`);
    }
  }

  // Cache failures briefly so a timeout doesn't pin a visitor to the base currency for a day
  currencyCache.set(ip, { value, expires: Date.now() + (value ? DAY_MS : 5 * 60 * 1000) });
  if (currencyCache.size > 50_000) currencyCache.delete(currencyCache.keys().next().value);
  return value;
}

// Step 3: exchange rates from CurrencyFreaks, cached for the plan's update interval
let ratesCache = { rates: null, expires: 0 };

async function getRates() {
  if (ratesCache.expires > Date.now()) return ratesCache.rates;

  const url = new URL("https://api.currencyfreaks.com/v2.0/rates/latest");
  url.searchParams.set("apikey", process.env.CURRENCYFREAKS_API_KEY ?? "");
  try {
    const res = await fetch(url, { signal: AbortSignal.timeout(5000) });
    const body = await res.json();
    if (!res.ok) throw new Error(`HTTP ${res.status}: ${body.message ?? "no message"}`);
    const rates = { ...body.rates, [body.base]: "1" }; // every rate is against body.base (USD)
    ratesCache = { rates, expires: Date.now() + DAY_MS }; // free plan: daily updates
  } catch (err) {
    console.error(`Rate refresh failed: ${err.message}`);
    ratesCache.expires = Date.now() + 10 * 60 * 1000; // keep the last good rates, retry later
  }
  return ratesCache.rates; // null until the first successful refresh
}

// Step 4: format with a locale the runtime supports
function usableLocale(tag) {
  if (!tag || tag === "*") return null;
  try {
    return Intl.NumberFormat.supportedLocalesOf(tag)[0] ?? null;
  } catch {
    return null; // malformed tag in Accept-Language
  }
}

function pickLocale(req, countryLanguages) {
  const candidates = [PAGE_LOCALE, ...req.acceptsLanguages(), ...countryLanguages, "en-US"];
  for (const tag of candidates) {
    const locale = usableLocale(tag);
    if (locale) return locale;
  }
  return "en-US";
}

function formatPrice(amount, currency, locale) {
  return new Intl.NumberFormat(locale, { style: "currency", currency }).format(amount);
}

// Step 5: a saved choice beats detection; no rate means the base currency
function resolveCurrency(saved, detected, rates) {
  for (const code of [saved, detected]) {
    if (!/^[A-Z]{3}$/.test(code ?? "")) continue;
    if (code === BASE_CURRENCY) return { currency: code, rate: 1 };
    // Cross rate, so a EUR or GBP base works with a USD-based feed
    const rate = Number(rates?.[code]) / Number(rates?.[BASE_CURRENCY]);
    if (Number.isFinite(rate) && rate > 0) return { currency: code, rate };
  }
  return { currency: BASE_CURRENCY, rate: 1 };
}

const app = express();
app.set("trust proxy", 1); // one proxy (your load balancer) in front; match your setup
app.use(cookieParser());

app.get("/pricing", async (req, res) => {
  // TEST_IP only replaces loopback addresses, so it can't leak into production traffic
  const isLocal = ["::1", "127.0.0.1", "::ffff:127.0.0.1"].includes(req.ip);
  const ip = isLocal && process.env.TEST_IP ? process.env.TEST_IP : req.ip;

  const [geo, rates] = await Promise.all([lookupCurrency(ip), getRates()]);
  const detected = COUNTRY_OVERRIDES[geo?.country] ?? geo?.currency;
  const { currency, rate } = resolveCurrency(req.cookies.currency, detected, rates);
  const locale = pickLocale(req, geo?.languages ?? []);
  const isEstimate = currency !== BASE_CURRENCY;

  const rows = PLANS.map((plan) => {
    const price = formatPrice((plan.priceMinor / 100) * rate, currency, locale);
    return `<li>${plan.name}: ${price}${isEstimate ? " (approx.)" : ""}</li>`;
  }).join("");
  const options = [...new Set([currency, ...SWITCHER])]
    .map((code) => `<option${code === currency ? " selected" : ""}>${code}</option>`)
    .join("");

  res.set("Cache-Control", "private, no-store"); // per-visitor prices: keep shared caches out
  res.type("html").send(`<!doctype html>
<html lang="en">
<h1>Pricing</h1>
<ul>${rows}</ul>
${isEstimate ? `<p>Prices in ${currency} are estimates. You're billed in ${BASE_CURRENCY}.</p>` : ""}
<form method="post" action="/currency">
  <select name="currency">${options}</select>
  <button>Change currency</button>
</form>
</html>`);
});

app.post("/currency", express.urlencoded({ extended: false }), (req, res) => {
  const code = String(req.body?.currency ?? "").toUpperCase();
  if (/^[A-Z]{3}$/.test(code)) {
    res.cookie("currency", code, {
      maxAge: 365 * DAY_MS,
      httpOnly: true,
      sameSite: "lax",
      secure: req.secure, // HTTPS-only cookie whenever the request came in over HTTPS
    });
  }
  res.redirect(303, "/pricing");
});

const server = app.listen(process.env.PORT ?? 3000, () => {
  console.log(`Listening on port ${server.address().port}`);
});

process.on("SIGTERM", async () => {
  server.close();
  await ipgeo.close(); // release the SDK client's connections on shutdown
});
```

* * *

## When the IP points to the wrong currency

The other mistake IPGeolocation.io's team sees most is trusting the IP's currency for visitors on a VPN. Every IP geolocation provider shares this limit: a VPN, proxy, or relay shows where its server is, not where its user is, and corporate networks that route traffic through another country do the same. A sample IP `2.56.188.34` geolocates to Dallas and returns `USD`, while its security data flags a VPN with a threat score of 80.

So the team recommends keeping the base currency for visitors flagged as VPN or anonymous, showing the switcher, and not treating VPN detection as proof of where someone is. Our security data, on paid plans, carries the flag for that: `is_anonymous` is true for VPN, proxy, Tor, and relay traffic, next to a 0 to 100 threat score and separate VPN, proxy, and residential proxy flags. In `lookupCurrency()`, replace the `lookupIpGeolocation()` call and the `value` assignment with:

```js
// Paid plans: include=security adds 2 credits per lookup
const { data } = await ipgeo.lookupIpGeolocation({
  ip,
  include: ["security"],
  fields: ["location.country_code2", "currency", "country_metadata.languages", "security.is_anonymous"],
});
const anonymous = data.security?.isAnonymous === true; // VPN, proxy, Tor, or relay
value = {
  currency: anonymous ? null : data.currency?.code || null, // keep the base currency
  country: data.location?.countryCode2 || null,
  languages: data.countryMetadata?.languages ?? [],
};
```

On a free key, the API rejects this request with a 401 and every visitor gets your base currency. With a paid key, the Dallas sample still sees dollars, now because it's flagged as anonymous: with `BASE_CURRENCY` set to `EUR`, it sees euros. [Credit costs for each module](https://ipgeolocation.io/documentation/credits-usage.html) show what the security data adds. At checkout, [real-time VPN and proxy detection](https://ipgeolocation.io/real-time-proxy-and-vpn-detection.html) adds a live connection test that reports the visitor's actual country.

![IPGeolocation.io response for a VPN exit in Dallas: currency USD with is_vpn and is_anonymous true and threat score 80](https://blogs.ipgeolocation.io/blogs-site/content/images/2026/10/vpn-ip-wrong-currency-security-data.webp)

* * *

## Before you ship local prices

*   Translate "(approx.)" and the billing note on localized pages.
*   If you store anything, store the currency code, not the IP address.
*   If prices must rank in each market, give each market its own URL with [hreflang annotations](https://ipgeolocation.io/guides/how-to-geo-redirect-visitors-to-localized-pages#using-hreflang-with-geo-redirects) instead of switching currency on one URL.
*   Test with documented or organization IPs, never a customer's.
*   On several app instances, move both caches from memory to Redis or another shared store.

* * *

## Going further with IPGeolocation.io

The lookup in Step 2 asked IPGeolocation.io for three fields, but the same request can return the rest of a visitor's IP data: geolocation, security, ASN, company, abuse contact data, time zone, and user agent, depending on your plan. So the visitor's time zone for a support-hours note, or the risk signals your signup form checks, can come from the lookup you already make.

Currency is one of the safer things to personalize by IP: it only needs the country, where IP geolocation is most reliable. [Our documentation](https://ipgeolocation.io/documentation/ip-location-api.html#frequently-asked-questions) puts typical country-level accuracy at 99%+.

A static pricing page with no server can still call the API from the browser on a paid plan: with [Request Origin](https://ipgeolocation.io/documentation/api-authentication.html), you register your domain, and requests from it need no key. [Our pricing page](https://ipgeolocation.io/pricing.html) shows what each plan includes.

## FAQ

### How do I detect a user's currency from their IP address?

**Answer:** Look up the IP with an IP geolocation API and read the currency code from the response. In IPGeolocation.io's API, `currency.code` holds the ISO 4217 code (EUR, JPY, BRL) in the default response, so no country-to-currency table is needed.

### How do I change the currency based on location on my website?

**Answer:** Detect the currency on your server from the visitor's IP, convert your base price with an exchange rate, and format it with `Intl.NumberFormat`. Save the visitor's own choice in a cookie and let it override detection. For a Shopify store, see the IPGeolocation.io tutorial on [adding geolocation to Shopify](https://ipgeolocation.io/tutorials/add-geolocation-to-shopify).

### Should I charge in the local currency or only display it?

**Answer:** Start by displaying it. IPGeolocation.io's team recommends showing converted prices in the visitor's currency while checkout stays in your base currency, with prices labeled as estimates and the checkout currency named. Add local-currency billing once your payment provider supports it.

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

**Answer:** Accurate for most visitors, but not for VPN or proxy traffic. Currency needs only the country, and [IPGeolocation.io's documentation](https://ipgeolocation.io/documentation/ip-location-api.html#frequently-asked-questions) puts typical country-level accuracy at 99%+. A VPN exit shows its server's country, so keep the base currency for anonymous visitors and always offer a switcher.

### Is there a free API to get the currency from an IP address?

**Answer:** Yes. IPGeolocation.io's free plan includes 1,000 credits per day with no credit card; the currency and country metadata objects are part of it, and a standard lookup costs 1 credit. [Paid plans](https://ipgeolocation.io/pricing.html) start at $19 per month and add security data, bulk lookups, and Request Origin for browser calls.

### Related Tutorials

[![Check if an IP Is a VPN, Proxy, or Tor Exit Node](https://blogs.ipgeolocation.io/blogs-site/content/images/2026/10/check-if-ip-is-vpn-proxy-or-tor-1.png)](https://ipgeolocation.io/tutorials/check-ip-for-vpn-proxy-tor)

[Check if an IP Is a VPN, Proxy, or Tor Exit Node](https://ipgeolocation.io/tutorials/check-ip-for-vpn-proxy-tor)

Check any IP for VPN, proxy, residential proxy, Tor, and relay flags with a browser lookup, one IPGeolocation.io API call, or the Tor Project's own exit list, then decide what each kind of match deserves.

Posted on October 27, 2026

By Abdul Mateen

[Read More](https://ipgeolocation.io/tutorials/check-ip-for-vpn-proxy-tor)

[![What Is My IP API? Get the Caller's Public IP with getip](https://blogs.ipgeolocation.io/blogs-site/content/images/2026/10/what-is-my-ip-api-feature.png)](https://ipgeolocation.io/tutorials/what-is-my-ip-api)

[What Is My IP API? Get the Caller's Public IP with getip](https://ipgeolocation.io/tutorials/what-is-my-ip-api)

Get your public IP from any script, server, or browser with IPGeolocation.io's getip endpoint. No API key or credits, and tested code for cURL, Python, Node.js, Go, and PHP.

Posted on October 21, 2026

By [Daud Ahmad](https://www.linkedin.com/in/daudahmad0303)

[Read More](https://ipgeolocation.io/tutorials/what-is-my-ip-api)

[![How to Add Geolocation to Your Shopify Store](https://blogs.ipgeolocation.io/blogs-site/content/images/2026/07/How-to-Add-Geolocation-to-Your-Shopify-Store.svg)](https://ipgeolocation.io/tutorials/add-geolocation-to-shopify)

[How to Add Geolocation to Your Shopify Store](https://ipgeolocation.io/tutorials/add-geolocation-to-shopify)

How to route Shopify visitors to the right market by country, and how to decide whether you need a geolocation app or Shopify's built-in redirect.

Posted on July 14, 2026

By [Anwar Ahmed](https://www.linkedin.com/in/anwar-ce/)

[Read More](https://ipgeolocation.io/tutorials/add-geolocation-to-shopify)

[![How to Get IP Geolocation in JavaScript (Node + Browser)](https://blogs.ipgeolocation.io/blogs-site/content/images/2026/06/ip-geolocation-in-javascript.png)](https://ipgeolocation.io/tutorials/ip-geolocation-in-javascript)

[How to Get IP Geolocation in JavaScript (Node + Browser)](https://ipgeolocation.io/tutorials/ip-geolocation-in-javascript)

Get IP geolocation in JavaScript the right way: the official SDK in Node.js, the correct browser pattern without exposing your key, the visitor's real IP via Express and Next.js, and the location, timezone, currency, ASN, VPN, proxy and threat data the API returns.

Posted on June 24, 2026

By Mudassar Tariq

[Read More](https://ipgeolocation.io/tutorials/ip-geolocation-in-javascript)

[![How to Get IP Geolocation in PHP (API Tutorial)](https://blogs.ipgeolocation.io/blogs-site/content/images/2026/06/ip-geolocation-in-php-1.png)](https://ipgeolocation.io/tutorials/ip-geolocation-in-php)

[How to Get IP Geolocation in PHP (API Tutorial)](https://ipgeolocation.io/tutorials/ip-geolocation-in-php)

Get IP geolocation in PHP with the official IPGeolocation.io library: install with Composer, run your first lookup, and read the real visitor IP in vanilla PHP and Laravel. Links to optional VPN/proxy, abuse contact, and bulk lookups on paid plans.

Posted on June 22, 2026

By Mudassar Tariq

[Read More](https://ipgeolocation.io/tutorials/ip-geolocation-in-php)

[![How to Get IP Geolocation in Python (API Tutorial)](https://blogs.ipgeolocation.io/blogs-site/content/images/2026/06/ipgeolocation-in-python-1.png)](https://ipgeolocation.io/tutorials/ip-geolocation-in-python)

[How to Get IP Geolocation in Python (API Tutorial)](https://ipgeolocation.io/tutorials/ip-geolocation-in-python)

Get IP geolocation in Python with the official IPGeolocation.io library: single lookups, async requests, and real visitor IP handling in Flask and FastAPI, plus optional bulk lookup and VPN/proxy detection examples.

Posted on June 19, 2026

By Mudassar Tariq

[Read More](https://ipgeolocation.io/tutorials/ip-geolocation-in-python)
