โ† All guides

๐Ÿ“ก 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 / Genderize / Nationalize alternative โ€” the shared 25-requests-a-day trap, and how to mock all three

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):

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.

1. Host your real test names in one curl

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.

2. Answer any name โ€” their exact shape, zero quota

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.

3. The translation table

agify.io familyYour 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 three10,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 extendPOST /agify {"count":1,"name":"pilar","age":30} โ€” persists, filterable immediately
429 {"error":"Request limit reached"} at the caprehearse it on purpose โ€” ยง4

4. Rehearse the 429 you'll hit in production

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.

Where the real APIs win โ€” and belong

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.

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: 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 โ†’

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