๐ก We check api.agify.io (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. This guide is about its free-tier quota, not availability.
Agify.io and its siblings genderize.io and nationalize.io are the canonical first-fetch exercise: no key, CORS open, type a name into a query param, get JSON back โ {"count":311558,"name":"michael","age":57}. They're real data products (hundreds of millions of name records) generously exposed keyless, and for the exercise as written they're lovely.
Three quota traps bite the moment the exercise becomes an app, a classroom, or a test suite (all checked live on September 18, 2026):
x-rate-limit-limit: 25. A "guess my age" demo that fetches as you type burns the whole day's quota in one form session; a classroom NAT, office network, or CI runner shares one IP across everybody.x-rate-limit-remaining tick down as we alternated agify โ genderize โ nationalize calls, then exhausted it on agify โ and genderize.io and nationalize.io immediately answered 429 too. One counter, three hostnames. A tutorial that demos all three spends one pool.?name[]=liam&name[]=yuki is one HTTP request but two credits โ we watched remaining drop from 22 to 20. Past the cap every call answers HTTP 429 {"error":"Request limit reached"} (still with CORS headers, to be fair) until the daily reset.None of that is a scandal โ 25/day is a fair keyless tier for a data product whose value is the data. The problem is where those 25 calls get spent: npm test, Storybook, a hot-reloading dev server, a workshop of thirty students. The fix: spend zero real calls on development. Host the handful of names your tests actually use โ their exact response shapes, verbatim โ on a free mock API, and point your dev/CI environment at it. No signup, no key.
These are genuine api.agify.io / genderize.io / nationalize.io responses (fetched once, September 18, 2026) with an id added โ swap in the names your app cares about:
cat > names.json <<'EOF'
{
"agify": [
{"id":1,"count":311558,"name":"michael","age":57},
{"id":2,"count":16860,"name":"sophia","age":41},
{"id":3,"count":72870,"name":"olga","age":53},
{"id":4,"count":1632,"name":"kwame","age":46},
{"id":5,"count":939,"name":"aiko","age":42},
{"id":6,"count":10934,"name":"liam","age":39},
{"id":7,"count":4193,"name":"yuki","age":40}
],
"genderize": [
{"id":1,"count":142897,"name":"sophia","gender":"female","probability":0.99}
],
"nationalize": [
{"id":1,"count":452,"name":"kowalski","country":[
{"country_id":"PL","probability":0.421455},
{"country_id":"FR","probability":0.089613},
{"country_id":"DE","probability":0.076975},
{"country_id":"US","probability":0.066769},
{"country_id":"ES","probability":0.031571}]}
]
}
EOF
curl -X POST "https://mockbird.mockbird.workers.dev/api/projects/import?name=names" \
-H 'content-type: application/json' --data-binary @names.json
# response includes your project id + admin key
curl "https://mockbird.mockbird.workers.dev/m/PROJECT_ID/agify?name=sophia"
# โ [{"id":2,"count":16860,"name":"sophia","age":41}] X-Total-Count: 1
curl "https://mockbird.mockbird.workers.dev/m/PROJECT_ID/nationalize?name=kowalski"
# โ the full nested country[] array, verbatim
Two honest differences from the real thing: a list endpoint answers with an array, so tutorial code reads (await r.json())[0].age instead of .age โ and a name you haven't seeded returns [] with X-Total-Count: 0 rather than a guess. For a bare-object response that answers any name, add the template route in ยง2.
A custom route with a template echoes whatever name is asked, in agify's bare-object shape, 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":"/guess",
"body":"{\"count\": 1000, \"name\": \"{{query.name}}\", \"age\": 34}",
"contentType":"application/json"}'
curl "https://mockbird.mockbird.workers.dev/m/PROJECT_ID/guess?name=anyname-at-all"
# โ {"count": 1000, "name": "anyname-at-all", "age": 34}
Every name is 34 years old in your test environment. Your snapshot tests stop flaking, your dev server stops burning quota, and the assertion "renders the age from the API" has a known answer.
| agify.io family | Your Mockbird project |
|---|---|
api.agify.io/?name=sophia | /agify?name=sophia (seeded names, real data, read [0]) or /guess?name=sophia (any name, bare object) |
api.genderize.io/?name=sophia | /genderize?name=sophia โ gender/probability fields verbatim |
api.nationalize.io/?name=kowalski | /nationalize?name=kowalski โ nested country[] array verbatim |
| 25 requests/day, shared across all three | 10,000/day per project โ a quota your test suite won't meet |
batch ?name[]=a&name[]=b (counts per name) | /agify?limit=100 lists everything you seeded in one call |
| fixed dataset you can't extend | POST /agify {"count":1,"name":"pilar","age":30} โ persists, filterable immediately |
429 {"error":"Request limit reached"} at the cap | rehearse it on purpose โ ยง4 |
Your real users will meet the cap โ 25/day is small. Does your app show something sensible? A drill route replays their exact limit response, headers included:
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":"/agify429","status":429,
"body":"{\"error\": \"Request limit reached\"}",
"contentType":"application/json",
"headers":{"x-rate-limit-limit":"25","x-rate-limit-remaining":"0","x-rate-limit-reset":"3600"}}'
curl -i "https://mockbird.mockbird.workers.dev/m/PROJECT_ID/agify429"
# โ HTTP 429, x-rate-limit-remaining: 0, {"error": "Request limit reached"}
# or drill retry logic: two 429s, then success
curl "https://mockbird.mockbird.workers.dev/m/PROJECT_ID/agify?name=sophia&mock_seq=429,429,200&mock_seq_key=quota"
Point the quota-warning banner, the retry/backoff path, and the "try again tomorrow" state at these URLs. More failure recipes in testing loading & error states and mocking rate limits.
Credit where due: the entire point of agify, genderize and nationalize is the prediction โ hundreds of millions of name records distilled into an answer. A mock cannot guess an age; it can only replay answers you seeded or a constant you chose. So keep the real APIs in production (their paid tier lifts the cap, and keyless is genuinely nice for a first lesson), and let the mock absorb everything that isn't production: local dev, CI, Storybook, workshops, load tests. The honest split: real predictions for real users โ agify.io and its paid tier. Every other request your stack makes โ your import, where the quota is effectively infinite 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: api.agify.io responses carried x-rate-limit-limit: 25; alternating calls across agify/genderize/nationalize decremented one shared x-rate-limit-remaining counter (24 โ 23 โ 22); a two-name batch cost two credits (22 โ 20); exhausting the pool on agify made genderize.io and nationalize.io answer HTTP 429 {"error":"Request limit reached"} on the next request. Every Mockbird command on this page was run against a live project before publishing (seeded-name lookup verbatim, nested country[] intact, template route echo, 429 drill route with headers, mock_seq 429/429/200, POST-a-name read-back), and the scratch project was deleted after.
Full API reference in the docs. More guides: Mock a rate-limited API ยท Custom endpoints ยท Mock API for React ยท Deterministic test data ยท Host a JSON file as an API. Create your API โ