๐ก 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 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):
https://ip-api.com/json/8.8.8.8 and you get HTTP 403 {"status":"fail","message":"SSL unavailable for this endpoint, order a key at https://members.ip-api.com/"}. And every site you'd call it from is served over HTTPS, where browsers block the http:// URL as mixed content before the request even leaves the page. CORS is open (Access-Control-Allow-Origin: * โ we checked), but it's open on an endpoint a modern web page can't reach. The frontend fetch in the tutorial only ever worked from http://localhost.192.168.x.x โ the first thing you'll test with on a LAN) โ both answer 200 OK with {"status":"fail","message":"private range", โฆ}. res.ok passes, your data.city is undefined, and the bug report says "city shows undefined for some users".X-Rl (requests remaining โ we watched it count down) and X-Ttl (seconds until the window resets). Their docs: go over and requests are throttled with HTTP 429; keep going and the IP is banned for an hour. A test suite, a hot-reloading dev server, or one office NAT spends that budget fast. And per their legal page, commercial use of the free endpoint requires the paid tier anyway.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.
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:
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.
| ip-api.com | Your 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 overruns | 10,000/day per project โ a quota your test suite won't meet |
fail = HTTP 200 + status:"fail" | rehearse it on purpose โ ยง4 |
| HTTPS = paid key | HTTPS included, like everything else on the internet since 2016 |
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.
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.
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 โ