Open Game Data
One endpoint, no key
Everything Wild Willows is built from is served from a single public URL: 150 real species with their diets, their food web and what each one needs before it will come home, across six habitat types. It is the same data the game itself runs on.
There is nothing to sign up for
No API key, no account and no dashboard to register in. One GET, from a browser or anywhere else, and you have the whole dataset. It sends Access-Control-Allow-Origin: *, so a page you are building can read it directly with no server of your own.
There is a rate limit: 60 requests a minute per address, counted at the origin — which cached requests never reach, so no honest use of a dataset that changes once per release will approach it. The numbers, and the arithmetic behind them, are below.
It exists because a classroom lesson needed a real API to teach against rather than a mock one. That turned out to be useful to more people than students, so it is documented rather than merely public.
Quick start
One endpoint. There are no parameters, no pagination and no other routes: you take the whole thing and pick what you want out of it.
GET https://wildwillows.app/GameData
JavaScript — browser or Node
const res = await fetch("https://wildwillows.app/GameData");
const data = await res.json();
const meadow = data.animals.filter((a) => a.biome === "meadow");
console.log(`${meadow.length} animals live in Willow Meadow`);
console.log(meadow.slice(0, 3).map((a) => `${a.name} — ${a.diet}`).join("\n"));
Python — standard library, nothing to install
import json, urllib.request
with urllib.request.urlopen("https://wildwillows.app/GameData") as r:
data = json.load(r)
meadow = [a for a in data["animals"] if a["biome"] == "meadow"]
print(f"{len(meadow)} animals live in Willow Meadow")
for a in meadow[:3]:
print(f"{a['name']} — {a['diet']}")
Both print
25 animals live in Willow Meadow Grasshopper — grasses, sedges and broadleaf forbs Prairie Vole — grasses, sedges, seeds and roots, plus bark in winter Monarch Butterfly — caterpillars eat only milkweed; adults sip flower nectar
curl
curl -s https://wildwillows.app/GameData \ | jq -r '.animals[] | select(.biome == "meadow" and .trophic == "apex-predator") | .name' Red-tailed Hawk Coyote Barn Owl
Every snippet on this page was run against the live data before it was published, and the output under each one is what it actually printed. If one of them stops working, that is a bug and I would like to hear about it.
What comes back
One JSON object. These are its keys, all of them arrays except the last two.
| Key | Count | What it holds |
|---|---|---|
animals | 150 | Every species, 25 per biome. The interesting one, and what the rest of this page is mostly about. |
biomes | 6 | The six habitat types, with their palettes, their grid size and what can be gathered in each. |
habitatObjects | 385 | Everything that can be planted or built, and what each one contributes to habitat. |
recipes | 355 | What each item is made from. |
resources | 38 | Gatherable materials, and which tool collects them. |
achievements | 50 | Named goals, their category and their requirement. |
tools | 9 | The tools and their upgrade tiers. |
appearanceOptions | — | An object of character-customization lists. Only interesting if you are building something that draws a caretaker. |
nodeRegenSeconds | — | A single number, a game-balance constant. Ignore it. |
Roughly half a megabyte of JSON, about a fifth of that over the wire once compressed. There is no thinner version: see using it kindly for how to fetch it once rather than often.
The animal object
The one worth knowing. Every field below is on every one of the 150 records, and none of them are placeholders: the diets, the shelters and the food-web links were written from cited sources, which travel with the record.
| Field | Type | What it is |
|---|---|---|
id | string | Stable kebab-case identifier, "red-fox". What eats, eatenBy and requirements.animals refer to. |
name | string | Common name, "Red Fox". |
scientificName | string | "Vulpes vulpes". |
biome | string | One of the six biome ids. Exactly 25 species per biome. |
kind | string | Broad group: mammal, bird, insect, invertebrate, reptile, amphibian, fish. |
trophic | string | Role in the food web. Nine values, listed below. |
rarity | string | common (69), uncommon (41) or rare (40). |
diet | string | A sentence, not a tag list. "grasses, sedges and broadleaf forbs". |
shelter | string | Where it nests, dens or hides, in a sentence. |
preferredHabitat | string | The conditions it wants, in a sentence. |
fact | string | One true, checkable thing about the species. Good for a card, a quiz or a fact-of-the-day. |
role | string | A paragraph on what it does in its ecosystem and who depends on it. |
eats | string[] | Ids of other animals in the dataset. Empty for a herbivore. |
eatsOther | string[] | Food that is not an animal in the dataset: ["grasses", "leaves and forbs", "sedges"]. |
eatenBy | string[] | Ids of its predators. Empty for an apex predator, which is a useful thing to check for. |
sources | object[] | { name, url } references for the claims above. Real citations, mostly to Animal Diversity Web and state wildlife agencies. |
requirements | object | What has to be true before this species returns. Its own section below. |
Trophic roles
eatenBy is empty for all of them.Requirements, and why they are the interesting part
Most species datasets tell you what an animal is. This one also encodes what a place has to become before that animal will live in it, because the game had to decide when each one comes back. That turns the dataset into something you can compute an order from.
"requirements": {
"minHealth": 65,
"minBalance": 40,
"objects": { "earthen-fox-den": 1, "native-grass-patch": 2, "brush-pile": 1, "oak-tree": 1 },
"signature": "earthen-fox-den",
"water": { "tiles": 1 },
"animals": ["cottontail-rabbit", "prairie-vole"],
"hint": "This one comes back once the small animals do — and she needs an old burrow she can widen into a nursery."
}
| Field | Type | What it is |
|---|---|---|
minHealth | number | Percentage of habitat recovery the biome needs first. 8 for the first arrival, 80 for the last. |
minBalance | number | How evenly spread that recovery has to be. Optional. |
objects | object | Habitat item id to how many are needed. The ids are in habitatObjects. |
signature | string | The one item most identified with this species, if it has one. |
water | object | { tiles: n } when it needs open water. Absent when it does not. |
animals | string[] | The good one. Other species that must already be present. This is what makes predators arrive last. |
hint | string | A written clue, in plain language, for a player working out what is missing. |
The biome object
| Field | Type | What it is |
|---|---|---|
id | string | meadow, forest, wetland, desert, alpine, coastal. |
name | string | Willow Meadow, Old Hollow Forest, Rushwater Wetland, Redstone Scrubland, Graywind Heights, Pelican Shore. |
order | number | The order they are restored in, 1 to 6. |
description | string | What the place is, and what happened to it. |
restorationGoal | string | What a player is being asked to do there. |
palette | object | { damaged, healthy }, two hex colors. Useful if you are drawing anything. |
grid | object | { cols, rows } — the meadow is 44 × 26. |
resources | string[] | What can be gathered on the surface there. |
digResources | string[] | What digging turns up. Repeats are weightings, not mistakes. |
unlock | object | What opens this biome. null for the meadow, which is where everyone starts. |
explorable | boolean | Whether it is a place you can go. |
Three things worth building
Each of these runs as written, and the output under it is what it printed.
1. The arrival ladder
Sort a biome by minHealth and you get the order life comes back in. Read requirements.animals alongside it and you can see why the hunters are at the bottom of the list.
const data = await (await fetch("https://wildwillows.app/GameData")).json();
const ladder = data.animals
.filter((a) => a.biome === "meadow")
.sort((a, b) => a.requirements.minHealth - b.requirements.minHealth);
const name = Object.fromEntries(data.animals.map((a) => [a.id, a.name]));
for (const a of ladder.slice(0, 8)) {
const waiting = (a.requirements.animals || []).map((id) => name[id]).join(", ");
console.log(`${String(a.requirements.minHealth).padStart(3)}% ${a.name}${waiting ? " (needs " + waiting + ")" : ""}`);
}
8% Grasshopper 9% Pillbug 10% Prairie Vole 10% Snail 12% Ladybug 14% Ground Squirrel (needs Grasshopper) 15% Monarch Butterfly 16% Song Sparrow (needs Grasshopper)
2. A census of the food web
import requests
from collections import Counter
data = requests.get("https://wildwillows.app/GameData", timeout=30).json()
counts = Counter(a["trophic"] for a in data["animals"])
for role, n in counts.most_common():
print(f"{role:16} {n}")
herbivore 46 insectivore 29 mesopredator 23 omnivore 18 apex-predator 17 detritivore 10 filter-feeder 4 scavenger 2 decomposer 1
3. Who eats whom
eats and eatenBy hold ids rather than names, so build the lookup once and the whole web is walkable. An empty eatenBy means you have reached the top.
const byId = Object.fromEntries(data.animals.map((a) => [a.id, a]));
const names = (ids) => ids.map((id) => byId[id].name).join(", ");
const fox = byId["red-fox"];
console.log(`${fox.name} eats ${names(fox.eats)}`);
console.log(`and is eaten by ${names(fox.eatenBy) || "nothing in this dataset"}`);
Every id in eats and eatenBy resolves to a record in the same response, so the graph is closed and you never have to handle a dangling reference.
Caching, compression and what a client sees
| Header | Value | What it means for you |
|---|---|---|
access-control-allow-origin | * | Read it straight from a browser. A plain fetch(url) with no custom headers is never preflighted, so there is nothing else to configure. |
cache-control | public, max-age=600, | Two answers in one header: your browser may keep it for ten minutes, a shared cache for a day. See what that means below. |
etag | build stamp | Send it back as If-None-Match and an unchanged catalog answers with a 304 and an empty body instead of half a megabyte. Cheap, not free: it is still a request and a round trip, so it is a fallback for code that has to check, not a substitute for keeping the result. |
content-encoding | br / gzip | Negotiated from your Accept-Encoding. Every HTTP client you are likely to use asks for this by default. |
vary | Accept-Encoding | So a shared cache does not hand a gzip body to a client that cannot read it. |
retry-after | 60 | Only on a 429. Wait that long and you come back to a full budget rather than to another refusal. |
What caching is, and which kind is doing what here
Caching is keeping a copy of an answer so you do not have to ask for it again. That is the whole idea. Everything below is a variation on where the copy is kept and how long it is trusted, and the reason anyone bothers is that asking again is slow, costs somebody money, and can fail. This response is about half a megabyte and changes when the game ships a build, which makes it close to the ideal thing to cache: large, identical for everybody, and rarely different.
There are three copies in play between this server and your code, and they are worth telling apart.
| Kind | Where the copy lives | What it is set to here |
|---|---|---|
| Browser cache private |
On the machine that asked. Yours alone, which is why it is called a private cache: it may hold a response nobody else is allowed to see. | Ten minutes. Press Run twenty times in a lesson and you send one request, not twenty. Ship a build and it is visible within ten minutes without anyone clearing anything. |
| Shared cache CDN / edge |
On a machine between you and the origin, holding one copy for everyone who asks. This site sits behind Cloudflare, so that machine is usually a few miles from you. | One day, and a deploy purges it. Most requests are answered here and never reach the database at all, which is what lets the limits below be generous. |
| Your own copy in your program |
A variable, a file on disk, a row in your database. Nothing to do with HTTP: you decide when to throw it away. | Up to you, and it is the one that matters. Fetch the catalog once when your program starts and keep it. That is the difference between one request and one per page view. |
How a cache decides it is still allowed to answer
Two mechanisms, and this endpoint uses both.
An expiry. cache-control carries a number of seconds, and until it runs out the cached copy is served with no request at all. max-age is the number for a private cache and s-maxage is the one for a shared cache, which is how one header gives your browser ten minutes and Cloudflare a day. Nothing goes over the network in that window, which is why it is the fastest possible answer and also why it is the one that can be out of date.
A validator. When the copy does age out, the cache does not start from nothing: it sends the etag it kept and asks whether that version is still current. If it is, the answer is 304 Not Modified with no body, and the old copy is reused. Half a megabyte becomes a few hundred bytes. It is not free, though, and this is the part that is easy to get wrong: a 304 is still a request, still a round trip, and still counts against the rate limit. Cheap is not the same as nothing.
Why any of this is your problem
- It is most of what makes a page feel fast. A cached response arrives in single-digit milliseconds. Half a megabyte over school wifi does not.
- It is what keeps a free API free. One classroom re-running a fetch example is thirty machines asking the same question at the same moment. Answered from a cache that costs nothing; answered from a database it is a bill and, eventually, a limit.
- It works offline, briefly. A response inside its expiry window is served even if the network is not there.
- And it is the classic source of “but I fixed that”. Every cache is a promise that something has not changed, so every cache can be wrong. That is what the purge on deploy and the ten-minute browser window are for: bounded staleness rather than none, chosen on purpose.
One place a cache cannot help the client, and it is worth knowing why: the classroom editor runs student code in a sandboxed frame on an opaque origin, so it sends a real request every time and never a conditional one. Those still land in the shared cache above, which is what keeps them off the database. It is the rate limit they count against, and that is what the arithmetic below is about.
Only GET and HEAD. There are no request parameters and no write operations, which keeps the public attack surface intentionally small.
Rate limits
One limit, on one endpoint, and it is sized against what actually reaches this server rather than what gets sent to the address. You have to be working at it to meet it.
| Limit | Value | Why that number |
|---|---|---|
| Per address, burst | 60 | What you can spend at once, from nothing. A whole classroom starting together fits inside it with room to spare. |
| Per address, sustained | 60 / min | What the burst refills to. Nothing that reaches this server honestly is a steady stream: it is spikes with quiet in between. |
| Refill | 1 / s | Continuous, not a reset on the minute. Thirty seconds of quiet buys back thirty requests. |
Where those numbers come from
Not from a guess, and not from the size of a classroom. The limit counts what reaches the origin, and almost nothing does. Requests are answered by the shared cache, usually a few miles from whoever asked; what this server actually meets is the requests that miss that cache.
So the number to size against is not “how much does a class send” but “how much can miss”. Cold cache, thirty students in one room pressing Run at the same moment: about thirty requests arrive here in a second or two, and then nothing for the rest of the day. That is the honest worst case, and the burst is twice it.
For the record, the classroom figure is real and it is bigger: the editor runs student code in a sandboxed frame on an opaque origin, so it shares no cache and never sends If-None-Match — measured in a browser, thirty edits produced thirty full responses and zero 304s — which puts a working class at around 600 requests a minute. Those are real requests. They land on the edge.
The short version of the whole page: the limit is not there to make you cache, because the cache is already doing that. It is there so that one address cannot pull half a megabyte in a loop and spoil the endpoint for everyone else.
If you are hitting this by accident the fix is almost always to fetch once and keep the result — unlike the editor, your code can. Sending the etag back makes a repeat cheap rather than free: half a megabyte becomes a few hundred bytes, but it is still a request, and the limit counts requests.
What a refusal looks like
A 429 with the CORS header on it, so it reaches your catch as a real status rather than as an opaque network failure, and a plain-language body you can show a user.
HTTP/1.1 429 Too Many Requests access-control-allow-origin: * retry-after: 60 cache-control: no-store Too many requests for the game catalog. It changes only when the game ships a build, so fetch it once and keep it. Wait a minute and try again.
const res = await fetch("https://wildwillows.app/GameData");
if (res.status === 429) {
const wait = Number(res.headers.get("retry-after") || 60);
console.log(`Rate limited. Try again in ${wait}s.`);
} else {
const data = await res.json();
}
Nothing else here is limited, because nothing else here exists: one endpoint, no auth, no writes. If you need a higher ceiling for something real, write to me — it is a number in a config file and I would rather raise it than have you work around it.
What can change, and what will not
This is a game's internal catalog that has been made public, and being honest about that is more useful than promising an API contract I would then have to keep.
- The URL. It is hardcoded into every copy of the game that has already shipped, so it cannot move.
- Species
ids. They are the keys the save files use. - The fields in the tables above. A test in this repo fails the build if one of them is renamed, and it names this page when it does.
- The six biome ids, and 25 species in each.
- Species being added or changed. Treat 150 as a floor rather than a constant.
- Wording of
diet,shelter,factandrole, which get corrected as sources are re-checked. - Balance numbers, including
minHealthand item costs. - Anything under
appearanceOptions,recipesorachievements— those follow the game.
If you build something that depends on this and you tell me what you are depending on, I will treat it as load-bearing. That is a better offer than a version number I might not honor. If what you have found is not a change but a mistake, ask me to fix it.
Something in the data is wrong. Now what?
Ask me to change it, and I will. The catalog is one file compiled from cited sources, so a correction is an edit to the source data rather than a note appended to a page: once it goes out, every copy of the game and everyone reading this endpoint gets the fixed version.
Email [email protected] with as much of this as you have. None of it is required, but each part is one fewer thing I have to work out:
| Tell me | Example | Why |
|---|---|---|
The id | red-fox | Names repeat across regions and ids do not. It is the first field on every record. |
| The field | diet, eats, fact | Says whether this is a wording fix, a food-web link, or a number. |
| What it should say | “also takes insects and fruit” | A proposed correction moves faster than a report that something is off. |
| Where that comes from | a link, a paper, a field guide | Every record carries its own sources, and a correction has to be able to join them. |
What happens next
I check the claim against the record’s existing sources and against yours. If they disagree I will tell you which one I went with and why. A fix lands in the data file, the tests that guard the shape of every record have to pass, and it ships with the next build — at which point the etag changes, the edge copy is purged, and your next request gets the new catalog without you doing anything.
Some things I will push back on, and it is fairer to say so here than in a reply. This is a game catalog: diets are simplified to what a ten-year-old can act on, one species eats a handful of others rather than forty, and the requirements are a designer’s model of succession rather than a published one. A correction that makes a record more accurate and still playable gets in. One that makes it more accurate and unplayable probably does not, and I will say which of the two I think it is.
Other kinds of request
- A species is missing. Send it with a source and a biome. The six biomes hold 25 each and that count is fixed, so an addition is usually a swap; tell me what you think it should replace and why.
- A field you wish existed. Say what you would do with it. Fields that carry a real use get added far more easily than fields that would be nice to have.
- You are depending on something. Not a correction, but the most useful mail I get. Tell me which fields your project reads and I will treat them as load-bearing rather than finding out by breaking you.
One person reads this inbox, so there is no ticket number and no service-level promise. There is a reply, and data corrections go to the front of the queue — a wrong fact that a class reads is worse than a bug in the game.
Using it, and using it kindly
Use it for whatever you like — a class project, a visualization, a bot, a game of your own, a paper. No permission needed and no attribution required, though a link back is appreciated and I would genuinely like to see what you make.
Fetch it once, not in a loop. The whole catalog arrives in one response and does not change between deploys, so there is never a reason to request it more than once per session. If you are serving it on to other people, cache it and honor the etag. The rate limit is set well above that, and nothing bad happens to you for sitting comfortably under it.
Not suitable as a scientific reference. It is a carefully-sourced game catalog: real species with real diets and real relationships, simplified enough to be played. The requirements are a game designer's model of succession, not a published one.
If this is your first API
There is a free ten-chapter lesson built on exactly this endpoint, with an editor that runs your code on a button press and explains errors in plain language. It was written for a high-school classroom and it works fine on your own.
Teaching with it? There is an educator guide with a lesson plan, an answer key and a troubleshooting table.
Built something?
I would like to see it, and if you are depending on part of this I would rather know than find out by breaking it. Bug reports about the data itself are especially welcome — a wrong diet is a thing I can fix.