โ† All guides

๐Ÿ“ก We check ip-api.com (and 75+ other public dev APIs) with a plain GET every 30 minutes โ€” see the live status page. As of publishing it's up and answering normally over plain HTTP. This guide is about its free-tier restrictions, not availability.

ip-api.com alternative โ€” the HTTPS-is-paid trap (and the HTTP-200-fail trap), and how to mock IP geolocation

ip-api.com is the IP-geolocation API in a decade of tutorials: no key, no signup, curl http://ip-api.com/json/8.8.8.8 answers instantly with country, city, coordinates, timezone, ISP and AS number. It has a tidy ?fields= projection, a batch endpoint, localized names โ€” as a data product it's genuinely good, and for a server-side curl the keyless tier is generous.

Three traps bite the moment it leaves the terminal (all checked live on September 18, 2026):

None of that is a scandal โ€” free HTTP lookups with fair limits is a reasonable deal, and the paid tier is how the data stays maintained. The problem is where those constraints bite: development, CI, demos, and any HTTPS-served frontend. The fix: spend zero real calls (and no HTTP-only URLs) on development. Host the handful of lookups your tests actually use โ€” their exact response shapes, verbatim โ€” on a free mock API that speaks HTTPS and CORS, and point dev/CI at it. No signup, no key.

1. Host real lookups in one curl

These are genuine ip-api.com responses (fetched once over plain HTTP, September 18, 2026) with an id added โ€” swap in the addresses your app cares about:

cat > geo.json <<'EOF'
{
 "geo": [
  {"id":1,"status":"success","country":"United States","countryCode":"US","region":"VA","regionName":"Virginia","city":"Ashburn","zip":"20149","lat":39.03,"lon":-77.5,"timezone":"America/New_York","isp":"Google LLC","org":"Google Public DNS","as":"AS15169 Google LLC","query":"8.8.8.8"},
  {"id":2,"status":"success","country":"Australia","countryCode":"AU","region":"QLD","regionName":"Queensland","city":"South Brisbane","zip":"4101","lat":-27.4766,"lon":153.0166,"timezone":"Australia/Brisbane","isp":"Cloudflare, Inc","org":"APNIC and Cloudflare DNS Resolver project","as":"AS13335 Cloudflare, Inc.","query":"1.1.1.1"},
  {"id":3,"status":"success","country":"United States","countryCode":"US","region":"CA","regionName":"California","city":"Berkeley","zip":"94709","lat":37.8806,"lon":-122.268,"timezone":"America/Los_Angeles","isp":"Quad9","org":"Quad9","as":"AS19281 Quad9","query":"9.9.9.9"},
  {"id":4,"status":"success","country":"The Netherlands","countryCode":"NL","region":"NH","regionName":"North Holland","city":"Amsterdam","zip":"1012","lat":52.3676,"lon":4.90414,"timezone":"Europe/Amsterdam","isp":"Wikimedia esams infra","org":"","as":"AS14907 Wikimedia Foundation Inc.","query":"185.15.59.224"}
 ]
}
EOF
curl -X POST "https://mockbird.mockbird.workers.dev/api/projects/import?name=ipgeo" \
  -H 'content-type: application/json' --data-binary @geo.json
# response includes your project id + admin key
curl "https://mockbird.mockbird.workers.dev/m/PROJECT_ID/geo?query=8.8.8.8"
# โ†’ [{"status":"success","country":"United States",โ€ฆ,"query":"8.8.8.8"}]   X-Total-Count: 1

# their ?fields=status,city,query projection โ†’ our ?select= (id always included)
curl "https://mockbird.mockbird.workers.dev/m/PROJECT_ID/geo?query=1.1.1.1&select=city,query"

This URL is HTTPS. It sends real CORS headers. A browser on an https page can fetch it. Two honest differences from the real thing: a list endpoint answers with an array, so code reads (await r.json())[0].city instead of .city โ€” and an address you haven't seeded returns [] with X-Total-Count: 0 rather than a lookup. For a bare-object response that answers any IP in their exact URL shape, add the template route:

2. Answer any IP โ€” their exact URL and shape, over HTTPS

A custom route reproduces ip-api's actual URL pattern (/json/<ip>) and bare-object response, echoing whatever address is asked โ€” deterministically, which is exactly what a UI test wants:

curl -X POST "https://mockbird.mockbird.workers.dev/api/projects/PROJECT_ID/routes" \
  -H 'x-admin-key: KEY' -H 'content-type: application/json' \
  -d '{"method":"GET","path":"/json/:ip",
       "body":"{\"status\":\"success\",\"country\":\"United States\",\"countryCode\":\"US\",\"region\":\"VA\",\"regionName\":\"Virginia\",\"city\":\"Ashburn\",\"zip\":\"20149\",\"lat\":39.03,\"lon\":-77.5,\"timezone\":\"America/New_York\",\"isp\":\"Mockbird Fixture ISP\",\"org\":\"Mockbird\",\"as\":\"AS0 Mockbird\",\"query\":\"{{params.ip}}\"}",
       "contentType":"application/json"}'

curl "https://mockbird.mockbird.workers.dev/m/PROJECT_ID/json/203.0.113.9"
# โ†’ {"status":"success",โ€ฆ,"city":"Ashburn","query":"203.0.113.9"}

Every visitor is in Ashburn in your test environment. The map component renders a known pin, the "near you" copy has a known answer, and nothing in CI talks to an HTTP-only endpoint.

3. The translation table

ip-api.comYour Mockbird project
http://ip-api.com/json/8.8.8.8 (HTTP only)https://โ€ฆ/m/YOUR_ID/json/8.8.8.8 โ€” same URL shape, real HTTPS + CORS
?fields=status,city,query?select=city,query on the seeded collection
batch POST /batch ["8.8.8.8","1.1.1.1"]/geo?limit=100 lists every seeded lookup in one call
45 requests/min per IP, 1-hour ban for repeat overruns10,000/day per project โ€” a quota your test suite won't meet
fail = HTTP 200 + status:"fail"rehearse it on purpose โ€” ยง4
HTTPS = paid keyHTTPS included, like everything else on the internet since 2016

4. Rehearse the failures your prod code will actually meet

The status:"fail"-inside-HTTP-200 shape is the one that ships bugs โ€” drill it with their exact body (note your assertion must check the body, because the HTTP layer says everything is fine):

curl -X POST "https://mockbird.mockbird.workers.dev/api/projects/PROJECT_ID/routes" \
  -H 'x-admin-key: KEY' -H 'content-type: application/json' \
  -d '{"method":"GET","path":"/jsonfail/:ip",
       "body":"{\"status\":\"fail\",\"message\":\"invalid query\",\"query\":\"{{params.ip}}\"}",
       "contentType":"application/json"}'

curl "https://mockbird.mockbird.workers.dev/m/PROJECT_ID/jsonfail/notanip"
# โ†’ HTTP 200, {"status":"fail","message":"invalid query","query":"notanip"}

The 403 someone will eventually cause by "fixing" the base URL to https, and the 429 with ip-api's real rate-limit headers:

# the exact SSL-unavailable 403
curl -X POST "https://mockbird.mockbird.workers.dev/api/projects/PROJECT_ID/routes" \
  -H 'x-admin-key: KEY' -H 'content-type: application/json' \
  -d '{"method":"GET","path":"/ssl403","status":403,
       "body":"{\"status\":\"fail\",\"message\":\"SSL unavailable for this endpoint, order a key at https://members.ip-api.com/\"}",
       "contentType":"application/json"}'

# throttled: 429 with X-Rl: 0 and X-Ttl telling you when the window resets
curl -X POST "https://mockbird.mockbird.workers.dev/api/projects/PROJECT_ID/routes" \
  -H 'x-admin-key: KEY' -H 'content-type: application/json' \
  -d '{"method":"GET","path":"/json429","status":429,"body":"","contentType":"text/plain",
       "headers":{"X-Rl":"0","X-Ttl":"42"}}'

# or drill retry/backoff inline: two 429s, then success
curl "https://mockbird.mockbird.workers.dev/m/PROJECT_ID/geo?query=8.8.8.8&mock_seq=429,429,200&mock_seq_key=rl"

Point the fallback path ("couldn't determine your location"), the retry logic, and the mixed-content incident runbook at these URLs. More failure recipes in testing loading & error states and mocking rate limits.

Where the real API wins โ€” and belongs

Credit where due: the entire point of ip-api.com is the database โ€” a mock cannot geolocate an address it hasn't seen; it can only replay lookups you seeded or a fixture you chose. So production lookups of real visitor IPs belong on a real geolocation service: ip-api's paid tier (which is what buys HTTPS and commercial use), or another provider whose free tier speaks HTTPS if that's a hard requirement. The honest split: real geolocation for real visitors โ†’ a real provider, over HTTPS, within its terms. Every request your stack makes before that โ€” local dev, CI, Storybook, demos, load tests โ€” โ†’ your import, where HTTPS is free, failures are on demand, and the answers never change under your tests.

Start now

The one-curl import in section 1, or zero setup at all:

curl https://mockbird.mockbird.workers.dev/m/demo/products?limit=3

Facts checked live on September 18, 2026: http://ip-api.com/json/8.8.8.8 answered 200 with X-Rl/X-Ttl rate-limit headers and Access-Control-Allow-Origin: *; the same URL over https:// answered 403 {"status":"fail","message":"SSL unavailable for this endpoint, order a key at https://members.ip-api.com/"}; /json/notanip and /json/192.168.1.1 both answered HTTP 200 with status:"fail" bodies (invalid query / private range); the 45/min limit, 429 throttle, and 1-hour ban are per their published docs, and the non-commercial free-tier term is per their legal page. Every Mockbird command on this page was run against a live project before publishing (import with zero warnings, seeded lookup verbatim with X-Total-Count 1, ?select= projection, /json/:ip template echo, HTTP-200 fail drill, 403 + 429 drill routes with headers, mock_seq 429/429/200, POST-a-fixture read-back), and the scratch project was deleted after.

Full API reference in the docs. More guides: Mock a geocoding API ยท Mock a rate-limited API ยท Custom endpoints ยท Mock any third-party API ยท Deterministic test data. Create your API โ†’

โšก Skip the terminal: this link opens the dashboard's import panel โ€” paste the geo.json from section 1 and you're done. No signup.