How to Migrate from ipinfo.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 ipinfo.io alternative comparison → and the head-on review of ipinfo.io vs IP Geo API →. Those two pages tell you whether to switch. This page tells you how — including the three field-shape gotchas no other migration guide is honest about.

TL;DR — most ipinfo.io → IP Geo API migrations land in half an engineering day. The real work is not the swap itself; it is the loc-string split, the ASN-org concatenation, and a rollback path you actually trust.

Who this guide is for

You currently call ipinfo.io via https://ipinfo.io/{ip}?token=$TOKEN (or the official Node / Python / Go SDK), you’ve decided that the USD invoice plus the paid Privacy Detection add-on costs more than it should, and you want a REST replacement that:

If those three boxes are unchecked — pause and read the vs comparison → first. The tradeoffs are real, especially if you depend on mobile-carrier classification or ipinfo.io’s downloadable database.

The 7-step migration checklist

  1. Inventory every call site that hits ipinfo.io or imports an ipinfo SDK.
  2. Map your fields to the ipinfo-compatibility response (?format=ipinfo).
  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 ipinfo.io responses.
  6. Cut over gradually — 10% → 50% → 100% of traffic over 48 hours.
  7. Decommission the ipinfo.io token — revoke, archive billing, delete the SDK.

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 "ipinfo\.io|ipinfo\.IPinfo|node-ipinfo|from ipinfo|github.com/ipinfo" -- ':!*.lock' ':!*.md'

Most teams find 2-8 call sites for ipinfo.io (it’s typically called from one or two services because the SDK is so simple). Make a list. Note for each: language, SDK version, fields consumed, and whether the result is cached.

Step 2 — Map the fields

ipinfo.io returns a flat JSON shape:

{
  "ip": "8.8.8.8",
  "city": "Mountain View",
  "region": "California",
  "country": "US",
  "loc": "37.4056,-122.0775",
  "org": "AS15169 Google LLC",
  "postal": "94043",
  "timezone": "America/Los_Angeles",
  "privacy": { "vpn": false, "proxy": false, "tor": false, "relay": false, "hosting": false }
}

IP Geo API ships a ?format=ipinfo 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 ipinfo path IP Geo API ?format=ipinfo Native ?format=ipgeo
IP ip ip ip
City city city location.city
Region region region location.region
Country country country country.iso_code
Lat/Lng loc (e.g. "37.40,-122.07") loc location.lat / location.lng
ASN+Org org (e.g. "AS15169 Google LLC") org network.asn + network.organization
Postal postal postal location.postal_code
Time zone timezone timezone location.timezone
VPN / Tor / proxy / hosting privacy.{vpn,proxy,tor,relay,hosting} privacy.{vpn,proxy,tor,relay,hosting} threat.is_{vpn,proxy,tor,datacenter}

Fields the shim does not cover (documented gaps): anycast flag, abuse contact block (ipinfo paid-only Abuse Contact API), domains reverse-DNS list (separate ipinfo product), and mobile_carrier.{name,mcc,mnc} (ipinfo Mobile Carrier add-on; our equivalent is on the Q3 2026 roadmap). If your code reads any of those, list them as blockers and decide per call site whether to drop the dependency or keep ipinfo.io for that path only (hybrid pattern — see the comparison page →).

Step 3 — Feature flag, then drop-in client

Python (was ipinfo.getHandler)

# before
import ipinfo
handler = ipinfo.getHandler(os.environ["IPINFO_TOKEN"])
res = handler.getDetails(ip)
country = res.country

# after — drop-in via the ipinfo-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": "ipinfo"},
        timeout=2.0,
    )
    r.raise_for_status()
    return r.json()

def lookup_country(ip: str) -> str:
    if USE_IPGEO:
        return _lookup(ip)["country"]   # flat shape — no rewrite
    return handler.getDetails(ip).country

The lru_cache decorator gives you a free in-process cache; replace with Redis SETEX 60 for multi-pod deployments.

Node / TypeScript (was node-ipinfo)

// before
import { IPinfoWrapper } from "node-ipinfo";
const ipinfo = new IPinfoWrapper(process.env.IPINFO_TOKEN!);
const res = await ipinfo.lookupIp(ip);

// after — drop-in
const cache = new Map<string, any>();
export async function geoLookup(ip: string) {
  if (process.env.USE_IPGEO_API !== "1") return ipinfo.lookupIp(ip);
  if (cache.has(ip)) return cache.get(ip);
  const r = await fetch(`https://api.ipgeo.10b.app/v1/${ip}?format=ipinfo`, {
    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 (was github.com/ipinfo/go/v2/ipinfo)

// after — drop-in via the ipinfo-compatibility shim
url := fmt.Sprintf("https://api.ipgeo.10b.app/v1/%s?format=ipinfo", 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 ipinfo.Core struct

Step 4 — Cache layer (the step everyone skips)

A naive 1-call-per-request integration will burn through paid-tier quotas in week one. ipinfo.io’s smoothed monthly limit hides this, but our daily ceilings make it visible immediately on the free tier. 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.

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:
    legacy = handler.getDetails(ip).country
    if SHADOW_MODE:
        try:
            new = _lookup(ip)["country"]
            if new != legacy:
                logger.warning("ipinfo-shadow-mismatch", extra={"ip": ip, "legacy": legacy, "new": new})
        except Exception as e:
            logger.error("ipinfo-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 org-name suffix can differ ("Google LLC" vs "GOOGLE-LLC"). 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 schedule: 10% → 50% → 100% over 48 hours. Watch error rate, p99 latency, and downstream conversion / fraud-rate dashboards for any regression.

Step 7 — Decommission

After 7 days at 100% with no rollback events, you can:

The size win is smaller than the MaxMind decommission (no multi-GB MMDB to delete), but you do shave one HTTP client and one auth-flow off your dependency graph.

Rollback plan

Keep the legacy ipinfo.io path behind the same feature flag for a minimum of 30 days. If you see a regression at any percentage, you can flip back with one config change and zero deploy. After 30 days of stability, prune the dead code in a separate PR.

Note: ipinfo.io tokens accept query-param OR Authorization: Bearer auth; we standardise on Bearer for IP Geo API. Make sure your reverse proxy / WAF is not stripping Authorization headers on the new vendor’s hostname before flipping above 10%.

The 7 gotchas teams hit in week one

  1. loc string parsing. ipinfo returns "37.40,-122.07"; if your code does loc.split(",") you’re fine, but if you previously parsed the SDK’s typed Location{Latitude, Longitude} object, the shim’s flat string is a behavioural change. Use ?format=ipgeo to get split fields.
  2. org concatenation. ipinfo returns "AS15169 Google LLC" as one string; some downstream code does org.split(" ", 1) to extract the ASN. The shim preserves the same concatenation, but you should silently switch consumers to the native network.asn integer field at the same time — concat-parsing is a known fragility point.
  3. No cache layer. Quota burn in 48 h. Add the cache before flipping the flag.
  4. Outbound HTTPS blocked. Production VPC egress rules deny api.ipgeo.10b.app. Get firewall change scheduled before cutover. Easy to forget when ipinfo.io’s hostname was already allowlisted.
  5. Authorization header stripped at the edge. Some CDNs / WAFs strip Authorization on calls to non-allowlisted hostnames. Test from prod before flipping >10%.
  6. CI / test environments. Shared test IPs (127.0.0.1, ::1) return 422 from the API; mock them in test or short-circuit before the call.
  7. GDPR DPIA refresh. Switching processor classes (US Delaware ipinfo.io → EU-only IP Geo API) usually triggers a one-page Article 30 update. Boring, but should be on the cutover checklist; it’s also the reason most teams started this migration in the first place.

What you’ll see in week two

Pairing pages

FAQ

How long does a real ipinfo migration take? For a single-stack web app with 2-6 call sites and a working CI: half an engineering day end-to-end. Multi-stack monorepos with 10+ call sites: 1-2 days, mostly in shadow-mode tuning.

Will my ipinfo-shaped tests still pass? Yes — if you’ve used dependency injection on the SDK. The compatibility shim returns the same flat JSON shape for the supported field set. For fields outside the shim (mobile carrier, abuse contact, domains), mock the new client path instead.

What about the SDK ergonomics — typed objects vs raw JSON? ipinfo’s official SDKs return typed Details / Core structs. We do not ship language SDKs in 2026 — the API is small enough that a 10-line client is faster than a SDK. If your codebase relied on typed accessors, write a thin adapter (~20 LOC) that unmarshals the JSON into your existing struct shape; this keeps the rest of the codebase unchanged.

What’s the rollback story if something goes wrong? The feature flag gives you a 1-second flip back to ipinfo.io. Keep the token and the SDK alive for at least 30 days post-cutover. Most teams keep them for 90 days for audit-trail reasons.

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), measure for a week, then move to the next. There is no all-or-nothing requirement.

What about the Lite database (the free CSV)? Different beast — that’s a downloadable file, not an API call. If your code consumes the ipinfo Lite CSV at build-time (common in static-site generators), the migration looks more like the migrate-from-MaxMind guide than this one. Ping us at hello@ipgeo.10b.app if that’s your shape.

Related migration & comparison reading

Industry deep-dives


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

Pairs with the full ipinfo.io alternative comparison page and the head-on IP Geo API vs ipinfo.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.