How to Migrate from ipstack 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 ipstack alternative comparison → and the head-on review of ipstack 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 ipstack → IP Geo API migrations land in half an engineering day. The real work is not the swap itself; it is the HTTP→HTTPS scheme flip on free-tier code paths, the connection.* block split that the Security-Module add-on glued together, and a rollback path you actually trust.

Who this guide is for

You currently call ipstack via http://api.ipstack.com/{ip}?access_key=$KEY (or the apilayer-published SDK), you’ve decided that the USD-Paddle invoice plus the paid Security Module add-on costs more than it 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 depend on ipstack’s global Anycast latency from non-EU clients or you already use multiple apilayer products under one dashboard.

The 7-step migration checklist

  1. Inventory every call site that hits api.ipstack.com or imports an apilayer SDK.
  2. Map your fields to the ipstack-compatibility response (?format=ipstack).
  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 ipstack responses.
  6. Cut over gradually — 10% → 50% → 100% of traffic over 48 hours.
  7. Decommission the ipstack access_key — revoke in the apilayer dashboard, 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 "api\.ipstack\.com|access_key=|apilayer" -- ':!*.lock' ':!*.md'

Most teams find 1-4 call sites for ipstack. The endpoint is so simple (one HTTP GET, one query param) that direct requests.get / fetch calls are common — there’s often no SDK to grep. Make a list. Note for each: language, scheme (http vs https), fields consumed, and whether the result is cached.

Watch-out: count test code separately from production. Test fixtures often hardcode http://api.ipstack.com/... URLs that won’t trip a TLS-only egress allowlist; production paths usually do. Both need switching, but the order matters for shadow-mode (Step 5).

Step 2 — Map the fields

ipstack returns a flat JSON shape, with two nested blocks (location, time_zone, currency, connection, security):

{
  "ip": "8.8.8.8",
  "type": "ipv4",
  "continent_code": "NA",
  "continent_name": "North America",
  "country_code": "US",
  "country_name": "United States",
  "region_code": "CA",
  "region_name": "California",
  "city": "Mountain View",
  "zip": "94043",
  "latitude": 37.4056,
  "longitude": -122.0775,
  "location": { "geoname_id": 5375480, "capital": "Washington D.C.", "languages": [{"code":"en","name":"English","native":"English"}] },
  "time_zone": { "id": "America/Los_Angeles", "current_time": "2026-05-09T09:03:42-07:00", "gmt_offset": -25200, "code": "PDT", "is_daylight_saving": true },
  "currency": { "code": "USD", "name": "US Dollar", "symbol": "$" },
  "connection": { "asn": 15169, "isp": "Google LLC" },
  "security": { "is_proxy": false, "proxy_type": null, "is_crawler": false, "is_tor": false, "threat_level": "low" }
}

IP Geo API ships a ?format=ipstack 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 ipstack path IP Geo API ?format=ipstack Native ?format=ipgeo
IP ip ip ip
Country code country_code country_code country.iso_code
Country name country_name country_name country.name
Region code region_code region_code region.iso_code
Region name region_name region_name region.name
City city city location.city
Postal zip zip location.postal_code
Lat latitude latitude location.lat
Lng longitude longitude location.lng
Time zone ID time_zone.id time_zone.id location.timezone
ASN connection.asn connection.asn network.asn
ISP connection.isp connection.isp network.organization
VPN / proxy security.is_proxy (paid) security.is_proxy (free) threat.is_proxy
Tor security.is_tor (paid) security.is_tor (free) threat.is_tor
Crawler / bot security.is_crawler (paid) security.is_crawler (free) threat.is_crawler
Threat level security.threat_level (paid) security.threat_level (free) threat.score

Fields the shim does not cover (documented gaps): location.languages array (apilayer-specific enrichment, low signal), location.capital and location.geoname_id (we do not import GeoNames), currency block (use a dedicated FX API such as exchangerate.host — ipstack’s currency block was a low-signal multi-vendor attachment), and the deprecated time_zone.current_time clock string (reconstruct on the client from gmt_offset). If your code reads any of those, list them as blockers and decide per call site whether to drop the dependency or keep ipstack for that path only (hybrid pattern — see the comparison page →).

Step 3 — Feature flag, then drop-in client

Python (was raw requests against api.ipstack.com)

# before
import os, requests
IPSTACK_KEY = os.environ["IPSTACK_ACCESS_KEY"]

def lookup_country(ip: str) -> str:
    r = requests.get(f"http://api.ipstack.com/{ip}", params={"access_key": IPSTACK_KEY}, timeout=2.0)
    r.raise_for_status()
    return r.json()["country_code"]

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

def lookup_country(ip: str) -> str:
    if USE_IPGEO:
        return _lookup(ip)["country_code"]   # flat shape — no rewrite
    r = requests.get(f"https://api.ipstack.com/{ip}", params={"access_key": IPSTACK_KEY}, timeout=2.0)
    r.raise_for_status()
    return r.json()["country_code"]

Note the scheme flip: the new client URL is https://, and the old fallback path should be moved to https:// before the shadow-mode comparison if your ipstack plan is Basic or higher. If you are still on Free (HTTP-only), keep http:// for ipstack but log a TODO — mixed-scheme code paths are a real source of CSP bugs in production.

Node / TypeScript (was raw fetch against api.ipstack.com)

// before
const r = await fetch(`http://api.ipstack.com/${ip}?access_key=${process.env.IPSTACK_ACCESS_KEY}`);
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.ipstack.com/${ip}?access_key=${process.env.IPSTACK_ACCESS_KEY}`);
    return r.json();
  }
  if (cache.has(ip)) return cache.get(ip);
  const r = await fetch(`https://api.ipgeo.10b.app/v1/${ip}?format=ipstack`, {
    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 ipstack-compatibility shim
url := fmt.Sprintf("https://api.ipgeo.10b.app/v1/%s?format=ipstack", 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 ipstack-shaped 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. ipstack’s monthly ceilings smear this over 30 days; 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:
    r = requests.get(f"https://api.ipstack.com/{ip}", params={"access_key": IPSTACK_KEY}, timeout=2.0)
    r.raise_for_status()
    legacy = r.json()["country_code"]
    if SHADOW_MODE:
        try:
            new = _lookup(ip)["country_code"]
            if new != legacy:
                logger.warning("ipstack-shadow-mismatch", extra={"ip": ip, "legacy": legacy, "new": new})
        except Exception as e:
            logger.error("ipstack-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"). The single biggest mismatch class for ipstack is the security.* block: ipstack’s free shadow path will return empty / null fields because the Security Module is paywalled, while IP Geo API returns populated values. Treat null-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 security.* block is more sensitive than ipstack’s paid Security Module on common abuse-feed-listed ranges (Spamhaus DROP overlap), so a 10-20% bump in is_proxy=true is normal and not a regression.

Step 7 — Decommission

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

  1. Revoke the access_key in the apilayer dashboard.
  2. Cancel the apilayer subscription (Paddle invoices stop on the next cycle).
  3. Remove the IPSTACK_ACCESS_KEY env var from CI / production / staging.
  4. Delete the legacy fallback branch from your code (keep the feature-flag scaffold for the next migration).
  5. Update your DPIA / Article 30 record — processor change from apilayer (US/AT) to corem6 BV (NL/EU).

The 7 gotchas teams hit in week one

  1. HTTP→HTTPS scheme flip on free-tier code paths. ipstack’s free plan is HTTP-only; production code that previously read http://api.ipstack.com/... will now hit https://api.ipgeo.10b.app/v1/.... Most CSP / mixed-content errors disappear silently the moment you flip — but if any test or staging environment still expects http://, the cutover lands with a 4xx storm. Audit every URL string before flipping the flag.
  2. security.* shape change. ipstack’s security block is empty / null on the free plan and only populated on Professional ($49.99/mo). The IP Geo API shim populates the same path on every tier. Code paths that branch on security == null to mean “no Security Module” will start triggering the populated-branch — which is usually what you want, but verify your fraud rules behave the way you expect when the flags arrive on every request.
  3. connection.asn integer vs string. ipstack returns connection.asn as an integer (15169); some downstream code does str(asn) or asn.zfill(...) and expects a string. The shim preserves the integer type — but if your code was tolerant of both and silently converted, the new shim’s strict typing may surface a pre-existing bug. Pin a unit test on the type before flipping.
  4. No cache layer. Quota burn in 48 h. Add the cache before flipping the flag.
  5. Outbound HTTPS blocked. Production VPC egress rules deny api.ipgeo.10b.app. Get firewall change scheduled before cutover. ipstack’s hostname (api.ipstack.com) was likely already allowlisted; the new hostname is not.
  6. Authorization header stripped at the edge. ipstack used a query-param access_key; IP Geo API uses a Bearer token in the Authorization header. Some CDNs / WAFs strip Authorization on calls to non-allowlisted hostnames. Test from prod before flipping >10%. (Workaround: pass the key as ?api_key=... query param instead — supported on every tier.)
  7. GDPR DPIA refresh. Switching processor classes (apilayer Vienna+Delaware → corem6 NL-only) 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 ipstack 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: 1-2 days, mostly in shadow-mode tuning. The HTTP→HTTPS scheme flip is the time sink, not the field-shape diff.

Will my ipstack-shaped tests still pass? Yes — the compatibility shim returns the same flat JSON shape for the supported field set. For fields outside the shim (location.languages, location.capital, location.geoname_id, the currency block), mock the new client path or move that logic to a dedicated FX / GeoNames data source.

What about the SDK ergonomics? ipstack does not ship a first-party SDK; most callers are raw requests.get / fetch. We do not ship language SDKs in 2026 either — the API is small enough that a 10-line client is faster than a SDK. Migration mostly amounts to changing two strings (URL and auth header).

What’s the rollback story if something goes wrong? The feature flag gives you a 1-second flip back to ipstack. Keep the access_key and the apilayer subscription 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 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 the apilayer multi-API dashboard? If you also use currencylayer, weatherstack, numverify, or another apilayer product, the convenience of one dashboard is real. The hybrid pattern is fine: keep ipstack for the legacy path, point new code at IP Geo API, and re-evaluate at next contract renewal. Most teams find the bill split is favourable within one billing cycle even keeping the ipstack base subscription.

What if my plan was Free (HTTP-only)? Then this migration is also a TLS upgrade for you — IP Geo API’s free tier serves HTTPS, so any browser-side code that was previously gated behind a $9.99/mo Basic upgrade now works on the free tier. This is the single most common reason teams in the first hour of evaluation hit this guide.

What about ipstack’s currency and location.languages blocks? Those were enrichment fields apilayer attached because they ship currencylayer / a languages API too. We do not import them — we believe IP→currency and IP→language are weak signals and should be replaced by Accept-Language header parsing and an explicit user-preference store. If you genuinely need IP→currency, exchangerate.host is a free EUR-billed alternative.

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 ipstack alternative comparison page and the head-on IP Geo API vs ipstack 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.