How to Migrate from ipgeolocation.io to IP Geo API in 2026: A Step-by-Step Drop-In Guide

7-minute read · 2026 code samples · honest rollback plan

This is the practical companion to the ipgeolocation.io alternative comparison → and the head-on review of ipgeolocation.io vs IP Geo API →. Those two pages tell you whether to switch. This page tells you how — including the three field-shape and packaging gotchas no other migration guide is honest about.

TL;DR — most ipgeolocation.io → IP Geo API migrations land in half an engineering day. The real work is not the swap itself; it is unbundling the Security API path (is_proxy/is_tor/is_anonymous move from a separately-billed SKU to inline fields), moving the API key out of the query string into the Authorization header, and a rollback path you actually trust.

Who this guide is for

You currently call ipgeolocation.io via https://api.ipgeolocation.io/ipgeo?apiKey=…&ip=… (and possibly /security, /timezone, /astronomy, /user-agent, or the /bulk endpoint), you’ve decided that the separately-billed Security API SKU, USD-Stripe invoices, and the api-key-in-URL log-leak surface cost more than they should, and you want a REST replacement that:

If those four boxes are unchecked — pause and read the vs comparison → first. The tradeoffs are real, especially if you actively use the bundled /timezone, /astronomy, /currency, or /user-agent endpoints, the multi-region edge for non-EU clients, or the connection_type (mobile / cable / dialup) classifier we don’t expose 1:1.

The 7-step migration checklist

  1. Inventory every call site that hits ipgeolocation.io, including the /security, /timezone, /astronomy, /user-agent, and /bulk sub-endpoints.
  2. Map your fields to the ipgeolocation-compatibility response (?format=ipgeolocation).
  3. Add a feature flag so you can switch any call site between providers.
  4. Wire a 60-second cache in front of the API client (in-memory or Redis).
  5. Deploy in shadow mode — call both, log differences, serve ipgeolocation.io responses.
  6. Cut over gradually — 10% → 50% → 100% of traffic over 48 hours.
  7. Decommission the ipgeolocation.io API key — revoke in the ipgeolocation dashboard, cancel both Standard and Security SKUs, archive USD invoices.

The rest of this post walks each step with copy-paste code.

Step 1 — Inventory call sites

Run this in the repo root before touching anything:

git grep -nE "ipgeolocation\.io|api\.ipgeolocation|apiKey=|/security|/astronomy" -- ':!*.lock' ':!*.md'

Most teams find 1-6 call sites: one for the main /ipgeo lookup, optionally one each for /security (threat flags), /timezone, /astronomy, /user-agent, and /bulk. The bundled-endpoint pattern is what makes ipgeolocation.io’s surface area larger than ipinfo or ipapi.co; audit each one and decide whether you need to retain it.

Watch-out: the apiKey is a query-param on every ipgeolocation.io call (?apiKey=YOUR_KEY). That means it lands in every place that logs URLs: nginx access logs, Cloudflare logs, your APM trace spans, Sentry breadcrumbs, browser history (if called from the client), and any reverse-proxy that records request lines. Treat the existing key as already-leaked. Generate a fresh one before decommission, but don’t trust it for anything sensitive in the meantime. The new client uses an Authorization header that does not appear in URL logs by default.

Step 2 — Map the fields

ipgeolocation.io returns a flat-with-some-nesting JSON shape on /ipgeo, with two parallel country-code fields (country_code2 ISO-2 + country_code3 ISO-3) and one nested block for time_zone.*:

{
  "ip": "8.8.8.8",
  "continent_code": "NA",
  "continent_name": "North America",
  "country_code2": "US",
  "country_code3": "USA",
  "country_name": "United States",
  "country_capital": "Washington, D.C.",
  "state_prov": "California",
  "district": "Santa Clara County",
  "city": "Mountain View",
  "zipcode": "94043",
  "latitude": "37.40599",
  "longitude": "-122.07786",
  "is_eu": false,
  "calling_code": "+1",
  "country_tld": ".us",
  "languages": "en-US,es-US,haw,fr",
  "country_flag": "https://ipgeolocation.io/static/flags/us_64.png",
  "geoname_id": "5375481",
  "isp": "Google LLC",
  "connection_type": "",
  "organization": "Google LLC",
  "asn": "AS15169",
  "currency": { "code": "USD", "name": "US Dollar", "symbol": "$" },
  "time_zone": {
    "name": "America/Los_Angeles",
    "offset": -8,
    "offset_with_dst": -7,
    "current_time": "2026-05-10 04:12:01.123-0700",
    "current_time_unix": 1778378521.123,
    "is_dst": true,
    "dst_savings": 1
  }
}

IP Geo API ships an ?format=ipgeolocation compatibility shim that returns the same flat shape so most call sites stop noticing the swap. The mapping for the fields ~95% of integrations rely on:

Your old code ipgeolocation.io path IP Geo API ?format=ipgeolocation Native ?format=ipgeo
IP ip ip ip
Country code (ISO-2) country_code2 country_code2 country.iso_code
Country code (ISO-3) country_code3 country_code3 country.iso_code_alpha3
Country name country_name country_name country.name
EU member flag is_eu is_eu country.in_eu
Calling code calling_code calling_code country.calling_code
Continent code continent_code continent_code country.continent_code
Region (state/province) state_prov state_prov region.name
City city city location.city
Postal zipcode zipcode location.postal_code
Lat latitude (string) latitude (string) location.lat (number)
Lng longitude (string) longitude (string) location.lng (number)
Time zone name time_zone.name time_zone.name location.timezone
UTC offset time_zone.offset time_zone.offset location.utc_offset
Is DST time_zone.is_dst time_zone.is_dst location.is_dst
ASN string asn (e.g. "AS15169") asn (string "AS15169") network.asn (integer 15169)
Org / ISP organization and isp organization and isp network.organization
Currency code currency.code currency.code country.currency.iso
VPN / proxy (Security API: /security + extra SKU) is_proxy (free, inline) threat.is_proxy
Tor (Security API: /security + extra SKU) is_tor (free, inline) threat.is_tor
Datacenter (Security API only) is_datacenter (free, inline) threat.is_datacenter
VPN (Security API only) is_vpn (free, inline) threat.is_vpn
Anonymous (Security API: is_anonymous) is_anonymous (free, inline) threat.is_anonymous

Fields the shim does not cover (documented gaps): country_capital and country_flag and geoname_id (we do not import the world-fact-book or GeoNames enrichment — these are weak signals for almost every product use), district (sub-region; below city granularity, low signal), country_tld (statically derivable from country_code2 if you really need it), languages comma-separated string (use Accept-Language request header parsing instead), connection_type (the dialup / cable / mobile classifier — we expose is_datacenter as a boolean instead), and the entire astronomy.*, currency.*-name/symbol, and time_zone.current_time / current_time_unix blocks (compute these client-side from time_zone.name with Intl.DateTimeFormat or the equivalent in your language). If your code reads any of those, list them as blockers and decide per call site whether to drop the dependency or keep ipgeolocation.io for that path only (hybrid pattern — see the comparison page →).

Step 3 — Feature flag, then drop-in client

Python (was raw requests against api.ipgeolocation.io)

# before
import os, requests

KEY = os.environ["IPGEOLOCATION_API_KEY"]

def lookup_country(ip: str) -> str:
    r = requests.get(
        "https://api.ipgeolocation.io/ipgeo",
        params={"apiKey": KEY, "ip": ip},
        timeout=2.0,
    )
    r.raise_for_status()
    return r.json()["country_code2"]

# after — drop-in via the ipgeolocation-compatibility shim
import os, requests
from functools import lru_cache

API_KEY = os.environ["IPGEO_API_KEY"]
USE_IPGEO = os.environ.get("USE_IPGEO_API", "0") == "1"   # feature flag

@lru_cache(maxsize=10_000)
def _lookup(ip: str) -> dict:
    r = requests.get(
        f"https://api.ipgeo.10b.app/v1/{ip}",
        headers={"Authorization": f"Bearer {API_KEY}"},
        params={"format": "ipgeolocation"},
        timeout=2.0,
    )
    r.raise_for_status()
    return r.json()

def lookup_country(ip: str) -> str:
    if USE_IPGEO:
        return _lookup(ip)["country_code2"]   # flat shape — no rewrite
    r = requests.get(
        "https://api.ipgeolocation.io/ipgeo",
        params={"apiKey": os.environ["IPGEOLOCATION_API_KEY"], "ip": ip},
        timeout=2.0,
    )
    r.raise_for_status()
    return r.json()["country_code2"]

Note the auth-shape change: the API key moves out of the query string into the Authorization: Bearer … header. This is the single biggest hardening win of the migration — the key stops appearing in nginx access logs, Cloudflare logs, APM trace spans, Sentry breadcrumbs, and browser history. If your edge or WAF strips Authorization headers on outbound calls, fall back to ?api_key=… (note: snake-case, distinct from ipgeolocation’s camelCase apiKey=) — supported on every tier.

Node / TypeScript (was raw fetch against api.ipgeolocation.io)

// before
const r = await fetch(
  `https://api.ipgeolocation.io/ipgeo?apiKey=${process.env.IPGEOLOCATION_API_KEY}&ip=${ip}`
);
const j = await r.json();

// after — drop-in
const cache = new Map<string, any>();
export async function geoLookup(ip: string) {
  if (process.env.USE_IPGEO_API !== "1") {
    const r = await fetch(
      `https://api.ipgeolocation.io/ipgeo?apiKey=${process.env.IPGEOLOCATION_API_KEY}&ip=${ip}`
    );
    return r.json();
  }
  if (cache.has(ip)) return cache.get(ip);
  const r = await fetch(
    `https://api.ipgeo.10b.app/v1/${ip}?format=ipgeolocation`,
    { headers: { Authorization: `Bearer ${process.env.IPGEO_API_KEY!}` } }
  );
  if (!r.ok) throw new Error(`ipgeo ${r.status}`);
  const j = await r.json();
  cache.set(ip, j);
  setTimeout(() => cache.delete(ip), 60_000);   // 60-s TTL
  return j;
}

Go

// after — drop-in via the ipgeolocation-compatibility shim
url := fmt.Sprintf("https://api.ipgeo.10b.app/v1/%s?format=ipgeolocation", ip)
req, _ := http.NewRequestWithContext(ctx, "GET", url, nil)
req.Header.Set("Authorization", "Bearer "+os.Getenv("IPGEO_API_KEY"))
resp, err := httpClient.Do(req)
// ... unmarshal into your existing ipgeolocation-shaped struct

Step 4 — Cache layer (the step everyone skips)

A naive 1-call-per-request integration will burn through ipgeolocation.io’s free 30K req/mo (≈1K/day) in the first hour of any production traffic. The Bronze tier ($15/mo for 150K req/mo) is generous on raw $/req but adds the Security API as a separate SKU (~$15/mo on top) the moment you need threat flags — and the Security API has its own independent quota, so a misbehaving caller on /security does not consume /ipgeo quota and vice versa. That sounds fine until you cache them differently. The good news: most production traffic is dominated by 1-5% of IPs (your bot crawler, your monitoring, your power users). A 60-second in-memory cache typically deflects 70-90% of calls at zero cost.

If you want strict cache-miss bounds, add a per-host concurrency limiter so only one in-flight call per IP is ever issued. Bonus: a single cached response on the new client covers what previously required two calls (/ipgeo + /security) on ipgeolocation.io, which roughly halves your effective hit-rate-adjusted call volume on the threat-detection path.

Step 5 — Shadow mode (the step that builds trust)

Before flipping any user-facing path: call both APIs and compare.

def lookup_country(ip: str) -> str:
    r = requests.get(
        "https://api.ipgeolocation.io/ipgeo",
        params={"apiKey": os.environ["IPGEOLOCATION_API_KEY"], "ip": ip},
        timeout=2.0,
    )
    r.raise_for_status()
    legacy = r.json()["country_code2"]
    if SHADOW_MODE:
        try:
            new = _lookup(ip)["country_code2"]
            if new != legacy:
                logger.warning("ipgeolocation-shadow-mismatch",
                               extra={"ip": ip, "legacy": legacy, "new": new})
        except Exception as e:
            logger.error("ipgeolocation-shadow-error",
                         extra={"ip": ip, "error": str(e)})
    return legacy

Run shadow mode for 24-48 hours. The mismatch rate on country-level data is typically <0.5% (mostly recent IP-block reassignments where one source is fresher). City-level is 1-3%. ASN naming is the noisiest signal — both providers ship the same numeric ASN, but the organization field differs in shape: ipgeolocation.io returns the organization as a single string with two parallel fields (isp vs organization, often identical, occasionally drifted); IP Geo API’s native shape splits this into network.asn (integer) + network.organization (string). The shim re-concatenates and exposes both isp and organization for compatibility, but the ASN+org formatting can differ slightly between providers ("Google LLC" vs "GOOGLE"). The single biggest mismatch class for ipgeolocation.io is the threat-flag block: the legacy free path does not include is_vpn / is_proxy / is_tor (those are Security-API only on ipgeolocation.io), while IP Geo API returns populated values inline. Treat absent-vs-populated as a known-good signal, not a mismatch.

For most fraud / analytics rules the numeric ASN is the only field that matters; pin your match logic to that.

Step 6 — Gradual cutover

Once shadow logs are clean, flip a percentage of traffic via your feature-flag system (LaunchDarkly, Unleash, or a hashed-IP rollout):

import hashlib

def use_ipgeo(ip: str, percent: int) -> bool:
    h = int(hashlib.md5(ip.encode()).hexdigest(), 16)
    return (h % 100) < percent

Recommended ladder: 10% → 50% → 100% over 48 hours. Watch your existing fraud-flag dashboards for unexpected spikes; the bundled threat-flag block exposes signals that ipgeolocation.io’s Standard plan (without the Security SKU) did not, so if you wire is_vpn=true into a soft-block rule you may see a 5-15% bump in flagged sessions. This is not a regression — it is the threat data you were paying for separately on the /security SKU, now bundled inline.

Step 7 — Decommission

Once 100% has been on IP Geo API for >7 days with no incidents:

  1. Revoke the ipgeolocation.io API key in the ipgeolocation dashboard.
  2. Cancel both subscriptions (Standard $15-40/mo for /ipgeo, and Security $15-40/mo for /security if you had it). The two SKUs bill separately on Stripe; cancelling Standard does not cancel Security.
  3. Remove the IPGEOLOCATION_API_KEY env var from CI / production / staging.
  4. Cancel the ipgeolocation Stripe USD invoice tracker (most teams forget the duplicate-invoice line until accounting flags it next quarter).
  5. Delete the legacy fallback branch from your code (keep the feature-flag scaffold for the next migration).
  6. Update your DPIA / Article 30 record — processor change from ipgeolocation (US) to corem6 BV (NL/EU).

The 7 gotchas teams hit in week one

  1. API key still in URL logs forever. Even after migration, the historical nginx, Cloudflare, APM, and Sentry logs contain every ?apiKey=… value you ever sent. Rotate the key in the ipgeolocation dashboard before decommission, then assume the old value is permanently leaked. The new client puts the key in an Authorization header that is not URL-logged by default.
  2. Two SKUs on Stripe, not one. ipgeolocation.io’s Security API is a separate Stripe subscription with a separate invoice line. If you only cancel the Standard plan, the Security SKU keeps billing for another month. Cancel both, then double-check by waiting one billing cycle.
  3. asn string vs integer. ipgeolocation.io returns "AS15169" (string with AS prefix). IP Geo API native returns 15169 (integer, no prefix); the shim preserves the "AS..." string format on the asn field but exposes the integer at network.asn. Code that does int(asn[2:]) on the legacy field continues to work; code that reads network.asn as a string will break. Pin a unit test on the type before flipping.
  4. latitude / longitude as strings. ipgeolocation.io returns lat/lng as strings ("37.40599"). IP Geo API native returns numbers (37.40599); the shim preserves the string format on latitude / longitude but exposes the numeric at location.lat / location.lng. Code that does parseFloat(j.latitude) continues to work; code that does arithmetic directly on the new path will break if you mixed paths.
  5. time_zone.current_time is a server-rendered timestamp. ipgeolocation.io renders time_zone.current_time as a human-readable string at request time; we do not — pick time_zone.name (always present) and compute the wall-clock time client-side with Intl.DateTimeFormat or pytz / zoneinfo. Tests that snapshot the literal string will break; tests that compute from the IANA zone name keep working.
  6. No cache layer. Quota burn in 4-6 hours on the free tier (1K/day cap). Add the cache before flipping the flag.
  7. Outbound HTTPS blocked. Production VPC egress rules deny api.ipgeo.10b.app. Get firewall change scheduled before cutover. ipgeolocation.io’s hostname (api.ipgeolocation.io) was likely already allowlisted; the new hostname is not. The same applies to your CSP if you call from the browser.

What you’ll see in week two

Pairing pages

FAQ

How long does a real ipgeolocation.io migration take? For a single-stack web app with 1-4 call sites and a working CI: half an engineering day end-to-end. Multi-stack monorepos with 10+ call sites that also use /security and /timezone: 1-2 days, mostly in shadow-mode tuning of the threat-flag rules. The duplicate-Stripe-cancel is the time sink most teams underestimate, not the field-shape diff — put it on the cutover checklist.

Will my ipgeolocation.io-shaped tests still pass? Yes — the compatibility shim returns the same flat JSON shape for the supported field set, including the country_code2 / state_prov / time_zone.name triplet that 95% of integrations rely on. For fields outside the shim (country_capital, country_flag, geoname_id, district, country_tld, languages, connection_type, astronomy.*, currency.name/symbol, time_zone.current_time), mock the new client path or move that logic to a dedicated reference-data source.

What about the bundled /timezone, /astronomy, /currency, /user-agent endpoints? We do not ship those. If you actively use them you have three options: (a) keep ipgeolocation.io for those specific paths only and swap just /ipgeo (hybrid pattern, lowest-risk), (b) move timezone to native Intl.DateTimeFormat / zoneinfo (free, deterministic), and pick a focused vendor for astronomy/currency if you actually need them, or © keep ipgeolocation.io entirely if /astronomy is load-bearing for your product — the migration math doesn’t favor swapping for one core endpoint while still depending on the bundle. The vs comparison has the decision tree.

What about the SDK ergonomics? ipgeolocation.io ships first-party SDKs for Java, PHP, Node, Python, Go, Ruby, .NET, and a few more. They are thin wrappers over the REST endpoint. Most callers can replace the SDK call with a 10-line requests.get / fetch against our endpoint without losing much. We do not ship language SDKs in 2026 — the API is small enough that a thin client is faster to maintain than vendor-side SDKs.

What’s the rollback story if something goes wrong? The feature flag gives you a 1-second flip back to ipgeolocation.io. Keep the (free or paid) ipgeolocation.io integration working for at least 30 days post-cutover; the Standard SKU is cheap enough at $15/mo to leave running as instant fallback insurance. The Security SKU you can cancel immediately at cutover if you’ve already validated the bundled threat block.

Can I migrate one service at a time? Yes — and it’s the recommended approach. Each call site is independent. Migrate the lowest-risk one first (often a dashboard analytics path or a server-side log enrichment job), measure for a week, then move to the next. There is no all-or-nothing requirement.

What about ipgeolocation.io’s /bulk endpoint? ipgeolocation.io’s /ipgeo/bulk accepts up to 50 IPs per call. We support the same workflow via a JSON POST to /v1/bulk with up to 100 IPs per call (paginate for larger batches). The response is a flat array; the per-IP response shape is identical to the single-lookup ?format=ipgeolocation response.

What if I was on the free tier? Then the migration math is even simpler — both providers offer 30K req/mo (≈1K/day) free with no card on file. Our free tier additionally bundles the threat block (is_vpn / is_proxy / is_tor / is_datacenter) which is paid-only on ipgeolocation.io. Side-project teams that “just need geo + light bot detection” usually find the migration is a net feature gain at zero cost change.

Why does ipgeolocation.io split Standard and Security at all? Historically the threat-detection data sources were licensed separately and ipgeolocation.io passed through that packaging. Our pricing posture is “threat is a baseline expectation in 2026, not an upsell” — we vertically integrated the threat data into one quota, one invoice, one response. That difference in posture is the single biggest reason teams hit this migration guide.

Related migration & comparison reading

Industry deep-dives


Last reviewed 2026-05-10 · IP Geo API team · Comments / corrections: hello@ipgeo.10b.app

Pairs with the full ipgeolocation.io alternative comparison page and the head-on IP Geo API vs ipgeolocation.io review.


Get early access — 50% off for 12 months

First 100 signups lock in 50% off any paid plan for the first year. No credit card required — we’ll email you at launch.