Skip to content
integration

How to add IP geolocation to your SaaS in 20 minutes

| 7 min read
World map with location pins showing IP geolocation data
Photo by NASA on Unsplash

Your SaaS shows USD to a user in Berlin. Your cookie banner appears for visitors in Texas. Your fraud system can't flag when a Nigerian IP uses a German billing address. Four features need IP geolocation, and you can add all four in 20 minutes with one API.

The API call

Every feature in this post starts with the same endpoint. Here's the raw curl:

curl -X POST https://api.botoi.com/v1/ip/lookup \
  -H "Content-Type: application/json" \
  -d '{"ip": "8.8.8.8"}'

Response:

{
  "success": true,
  "data": {
    "ip": "8.8.8.8",
    "city": "Mountain View",
    "region": "California",
    "country": "US",
    "countryName": "United States",
    "latitude": 37.386,
    "longitude": -122.0838,
    "timezone": "America/Los_Angeles",
    "isp": "Google LLC",
    "org": "Google Public DNS",
    "as": "AS15169 Google LLC"
  }
}

One POST gives you city, region, country code, full country name, coordinates, timezone, ISP, organization, and AS number. That's enough data to power all four features below.

World map with network connection lines
Photo by NASA on Unsplash

Feature 1: Auto-select currency at checkout

Showing the wrong currency at checkout kills conversion rates. A visitor from Germany sees "$49.99" and has to mentally convert to euros before deciding. Worse, they might assume you don't serve their region.

Fix this with middleware that maps the visitor's IP country to a default currency:

const COUNTRY_CURRENCY = {
  US: "USD", GB: "GBP", DE: "EUR", FR: "EUR", JP: "JPY",
  IN: "INR", BR: "BRL", AU: "AUD", CA: "CAD", CN: "CNY",
  KR: "KRW", MX: "MXN", SE: "SEK", CH: "CHF", SG: "SGD",
};

async function currencyMiddleware(req, res, next) {
  const ip = req.headers["x-forwarded-for"]?.split(",")[0] || req.ip;

  try {
    const response = await fetch("https://api.botoi.com/v1/ip/lookup", {
      method: "POST",
      headers: {
        "Content-Type": "application/json",
        "X-Api-Key": process.env.BOTOI_API_KEY,
      },
      body: JSON.stringify({ ip }),
    });

    const { data } = await response.json();
    req.defaultCurrency = COUNTRY_CURRENCY[data.country] || "USD";
  } catch {
    req.defaultCurrency = "USD";
  }

  next();
}

// Usage in Express
app.get("/checkout", currencyMiddleware, (req, res) => {
  res.render("checkout", { currency: req.defaultCurrency });
});

The country-to-currency map covers the top 15 SaaS markets. Extend it for your audience. The fallback to USD handles API failures gracefully; no visitor ever sees a broken checkout because a geolocation call timed out.

Feature 2: GDPR cookie banner for EU visitors only

Showing a cookie consent banner to every visitor is unnecessary and annoying. GDPR applies to visitors in the European Union. Everyone else can skip it.

This middleware checks the visitor's IP country against the list of EU member states and sets a cookie the frontend reads:

const EU_COUNTRIES = new Set([
  "AT", "BE", "BG", "HR", "CY", "CZ", "DK", "EE", "FI", "FR",
  "DE", "GR", "HU", "IE", "IT", "LV", "LT", "LU", "MT", "NL",
  "PL", "PT", "RO", "SK", "SI", "ES", "SE",
]);

async function gdprMiddleware(req, res, next) {
  const ip = req.headers["x-forwarded-for"]?.split(",")[0] || req.ip;

  try {
    const response = await fetch("https://api.botoi.com/v1/ip/lookup", {
      method: "POST",
      headers: {
        "Content-Type": "application/json",
        "X-Api-Key": process.env.BOTOI_API_KEY,
      },
      body: JSON.stringify({ ip }),
    });

    const { data } = await response.json();
    req.isEU = EU_COUNTRIES.has(data.country);
  } catch {
    // Default to showing the banner when the lookup fails
    req.isEU = true;
  }

  next();
}

// Set a cookie so the frontend knows whether to show the banner
app.use(gdprMiddleware, (req, res, next) => {
  res.cookie("gdpr_applies", req.isEU ? "1" : "0", {
    httpOnly: false,
    maxAge: 86400 * 1000,
  });
  next();
});

On the frontend, read the cookie and toggle the banner:

// Frontend: read the cookie and conditionally show the banner
function shouldShowCookieBanner() {
  const match = document.cookie.match(/gdpr_applies=(d)/);
  return match ? match[1] === "1" : true; // default to showing
}

if (shouldShowCookieBanner()) {
  document.getElementById("cookie-banner").style.display = "block";
}

The default behavior on failure is to show the banner. This errs on the side of compliance; if the geo lookup fails, you still satisfy GDPR requirements. The cookie lasts 24 hours, so you only call the API once per visitor per day.

Feature 3: Fraud detection with geo mismatch

When someone checks out with a billing address in Germany but their IP geolocates to Nigeria, that's a signal worth investigating. This doesn't mean the transaction is fraudulent; people travel, use VPNs, and buy gifts for friends abroad. But it's a data point your fraud review team needs.

async function checkGeoMismatch(ip, billingCountry) {
  const response = await fetch("https://api.botoi.com/v1/ip/lookup", {
    method: "POST",
    headers: {
      "Content-Type": "application/json",
      "X-Api-Key": process.env.BOTOI_API_KEY,
    },
    body: JSON.stringify({ ip }),
  });

  const { data } = await response.json();

  const mismatch = data.country !== billingCountry;

  return {
    mismatch,
    ipCountry: data.country,
    ipCity: data.city,
    billingCountry,
    riskNote: mismatch
      ? `IP located in ${data.countryName} but billing address is ${billingCountry}`
      : null,
  };
}

// Usage during checkout
app.post("/checkout", async (req, res) => {
  const ip = req.headers["x-forwarded-for"]?.split(",")[0] || req.ip;
  const { billingCountry } = req.body;

  const geo = await checkGeoMismatch(ip, billingCountry);

  if (geo.mismatch) {
    // Flag for manual review instead of blocking
    await flagOrder(req.body.orderId, geo.riskNote);
  }

  // Continue processing the order
  await processOrder(req.body);
  res.json({ success: true });
});

The function returns a structured object with the mismatch flag and a human-readable risk note your support team can review. Flag the order for manual review instead of blocking it outright. Combine this with other signals (email domain age, payment velocity, device fingerprint) for a more complete picture.

Feature 4: Analytics dashboard with user distribution

Knowing where your users are helps you decide which languages to support, which regions to target with marketing, and where to place edge servers. This script processes a batch of visitor IPs and produces a sorted country distribution:

async function buildCountryDistribution(ips) {
  const counts = {};

  // Process in batches to respect rate limits
  for (const ip of ips) {
    try {
      const response = await fetch("https://api.botoi.com/v1/ip/lookup", {
        method: "POST",
        headers: {
          "Content-Type": "application/json",
          "X-Api-Key": process.env.BOTOI_API_KEY,
        },
        body: JSON.stringify({ ip }),
      });

      const { data } = await response.json();
      const country = data.countryName || "Unknown";
      counts[country] = (counts[country] || 0) + 1;
    } catch {
      counts["Unknown"] = (counts["Unknown"] || 0) + 1;
    }
  }

  // Sort by count descending
  return Object.entries(counts)
    .sort(([, a], [, b]) => b - a)
    .map(([country, count]) => ({
      country,
      count,
      percentage: ((count / ips.length) * 100).toFixed(1) + "%",
    }));
}

// Example output:
// [
//   { country: "United States", count: 4521, percentage: "34.2%" },
//   { country: "Germany", count: 1893, percentage: "14.3%" },
//   { country: "United Kingdom", count: 1247, percentage: "9.4%" },
//   ...
// ]

Run this as a nightly job against your access logs. The output tells you exactly which countries drive the most traffic. If 14% of your users are in Germany but your app only supports English, that's a localization opportunity you can quantify.

Caching strategy

IP-to-location mappings don't change often. There's no reason to call the API again for the same IP within a session. This cache uses a simple Map with a 1-hour TTL:

class GeoCache {
  constructor(ttlMs = 60 * 60 * 1000) {
    this.cache = new Map();
    this.ttlMs = ttlMs;
  }

  get(ip) {
    const entry = this.cache.get(ip);
    if (!entry) return null;
    if (Date.now() - entry.timestamp > this.ttlMs) {
      this.cache.delete(ip);
      return null;
    }
    return entry.data;
  }

  set(ip, data) {
    this.cache.set(ip, { data, timestamp: Date.now() });
  }
}

const geoCache = new GeoCache();

async function lookupWithCache(ip) {
  const cached = geoCache.get(ip);
  if (cached) return cached;

  const response = await fetch("https://api.botoi.com/v1/ip/lookup", {
    method: "POST",
    headers: {
      "Content-Type": "application/json",
      "X-Api-Key": process.env.BOTOI_API_KEY,
    },
    body: JSON.stringify({ ip }),
  });

  const { data } = await response.json();
  geoCache.set(ip, data);
  return data;
}

For single-instance Node.js servers, the in-memory Map works fine. If you run multiple instances behind a load balancer, swap the Map for Redis. The same TTL logic applies; store the geo data as a JSON string with an expiry of 3600 seconds.

Extracting the client IP

The trickiest part of IP geolocation isn't the API call; it's getting the correct IP in the first place. If your app sits behind a reverse proxy, load balancer, or CDN, req.connection.remoteAddress returns the proxy's IP, not the visitor's.

Here's how to get the real client IP in each environment:

// Express
const ip = req.headers["x-forwarded-for"]?.split(",")[0]?.trim() || req.ip;

// Next.js (App Router)
import { headers } from "next/headers";
const headerList = await headers();
const ip = headerList.get("x-forwarded-for")?.split(",")[0]?.trim();

// Cloudflare Workers
const ip = request.headers.get("cf-connecting-ip");

// Vercel (edge or serverless)
const ip = request.headers.get("x-real-ip")
  || request.headers.get("x-forwarded-for")?.split(",")[0]?.trim();

Always split on the first comma in x-forwarded-for. This header can contain a chain of IPs when traffic passes through multiple proxies. The first entry is the original client IP.

If you're on Cloudflare, use cf-connecting-ip. Cloudflare sets this header on every request and it's harder to spoof than x-forwarded-for.

Frequently asked questions

How do I add IP geolocation to my SaaS application?
Send your visitor's IP to an IP geolocation API (like POST /v1/ip/lookup) and use the returned country, city, and timezone data to personalize their experience. Common use cases include auto-selecting currency at checkout, showing GDPR banners to EU visitors, flagging geo-mismatch fraud, and building analytics dashboards. You can add all four features with a single API.
What is the best IP location API for SaaS products?
Look for an API that returns country, city, region, coordinates, timezone, and ISP data in a single call. Botoi's /v1/ip/lookup returns all of these fields with no signup required for anonymous access (5 req/min). For production use, a free API key provides 1,000 requests per day. Paid plans start at $9/month.
Can I geolocate users by IP without Google Maps?
Yes. IP geolocation APIs return latitude, longitude, city, and country data without requiring Google Maps or any mapping service. You only need a mapping library if you want to display the location on a visual map. For features like currency defaults, GDPR compliance, and fraud detection, the raw geolocation data from the API is all you need.
How accurate is IP geolocation for detecting user location?
IP geolocation is accurate to the country level about 99% of the time and to the city level about 80-90% of the time. Accuracy drops for mobile carriers and VPN users. For SaaS features like currency selection and GDPR compliance, country-level accuracy is sufficient. For fraud detection, combine IP geolocation with billing address data rather than relying on city-level precision.
Should I cache IP geolocation results in my SaaS?
Yes. IP-to-location mappings change infrequently, so caching results for 1 hour per IP reduces API calls significantly. Use an in-memory Map for single-instance deployments or Redis for multi-instance setups. Most SaaS applications see 60-80% cache hit rates because returning visitors hit the same IP within a session.

Try this API

IP Geolocation API — interactive playground and code examples

More integration posts

Start building with botoi

150+ API endpoints for lookup, text processing, image generation, and developer utilities. Free tier, no credit card.