Developers
The Wikiteadia API
Tea houses, festivals, teas, regions and everything else on the wiki, as JSON. No key, no quota, no sign-up and no terms to click through. Call it from a script, a server or straight from a browser tab: every endpoint is open to every origin.
We keep it that way on purpose. Wikiteadia is a reference work, and we think a reference work should be free to build on. The catch is that with no keys we have no idea who is using it, so we can't email you before we change something. That's what this page is for. If it's written here, we keep it working. If it isn't, it might change, so please don't lean on it.
curl https://wikiteadia.org/api/v1/places?city=lisbonGround rules
A few things hold for every endpoint. Knowing them up front will save you a surprise or two.
- Read-only
- Everything is
GET. The only way to add or change anything is the form on the site, where a reviewer sees it. slugis forever- Use it as your key. We never rename one, because your favourites, check-ins and bookmarks hang off it and we can't migrate data that lives in your app.
- Missing means unknown
- A field we don't know is left out of the response, never sent as
null. Check for the key, and your templates will never print the word “null” at anyone. - Forgiving parameters
- A filter that makes no sense is ignored, and a limit that's too big is clamped. You get an answer you can see is wrong and fix, rather than a 500 that looks like our fault.
- No UUIDs
- Everything links by
slug. To follow a relation, ask for that kind's category and join on it. - Versioned paths
- Everything under
/api/v1is covered by this page./search?format=jsonis not: the site uses it internally and it changes whenever the search page does.
All endpoints
| Endpoint | Returns | Cache |
|---|---|---|
GET /api/v1/places | Tea houses and tea shops, with coordinates | 1 hour |
GET /api/v1/{category} | Every published subject of one category | 1 hour |
GET /api/v1/festivals | Tea festivals, with dates, venues and coordinates | 1 hour |
GET /tea-festivals-{year}.geojson | That year's festival editions as GeoJSON | 5 minutes |
GET /tea-festivals-{year}.ics | The same editions as a calendar feed | 1 hour |
Cache times are s-maxage, which applies to shared caches only: a CDN holds the response, your browser refetches.
01
GET /api/v1/places
Every published tea house and tea shop that has the basics a listing needs: a name, a location and a city.
Requests
GET https://wikiteadia.org/api/v1/places
GET https://wikiteadia.org/api/v1/places?city=lisbon
GET https://wikiteadia.org/api/v1/places?country=pt&limit=50Parameters
| Name | Shape | Default |
|---|---|---|
city | a lowercase slug: lisbon, kyoto, hackney | all cities |
country | ISO 3166-1 alpha-2, either case | all countries |
limit | 1 to 2000 | 500 |
Response
{
"places": [
{
"slug": "cha-do-adro",
"name": "Chá do Adro",
"kind": "tea_house",
"lat": 38.7223,
"lng": -9.1393,
"city_slug": "lisbon",
"country_code": "PT",
"timezone": "Europe/Lisbon",
"url": "https://wikiteadia.org/place/cha-do-adro",
"address": "Rua do Adro 12, Lisboa",
"opening_hours": "Tu-Su 11:00-19:00",
"hours": [[2, "11:00", "19:00"]],
"website": "https://chadoadro.pt",
"teas": ["Gyokuro", "Sencha"]
}
],
"count": 1,
"generated_at": "2026-09-20T11:00:00.000Z"
}Good to know
- Eight fields are always there:
slug,name,lat,lng,city_slug,country_code,timezoneandurl. The rest appear when we know them. opening_hoursis the source,hoursis our parse of it. The raw string is what a contributor copied off the shop window, in OSM syntax.hoursturns it into[day, open, close]triples, with day 0 as Sunday. If we can't parse the whole week, we leavehoursout rather than guess, and you still get the raw string.- No ranking, ever. There's no
featured,rankorsponsoredfield. Which tea house is worth an evening is your call as the publisher of your guide, not a fact about the building.
02
GET /api/v1/{category}
Every published subject in one category, exactly as a reviewer approved it. The category is the same word the site uses in its URLs, so a page you can read is a page you can parse: /teas in a browser, /api/v1/teas in a script.
Categories today
teasregionsplacesproducerscultivarsprocessesbrewingtopicspeopleworkswordsships
Please don't hard-code that list. It grows whenever we add a kind of page. If you ask for one that doesn't exist, the 404 tells you what does:
{ "error": "unknown category", "categories": ["teas", "regions", "…"] }places is the exception. It answers with its own shape, documented above, because it was built for a specific app before this endpoint existed and we don't swap out a shape someone relies on.
Parameters
| Name | Shape | Default |
|---|---|---|
limit | 0 to 500 | 100 |
offset | any non-negative integer | 0 |
Both are clamped. Ask for ?limit=99999 and you get 500 rows, with "limit": 500 in the body to tell you so.
Response
{
"category": "teas",
"kind": "tea",
"total": 412,
"limit": 100,
"offset": 0,
"subjects": [
{
"slug": "da-hong-pao",
"name": "Da Hong Pao",
"kind": "tea",
"url": "https://wikiteadia.org/tea/da-hong-pao",
"updated_at": "2026-08-02T11:14:52.310Z",
"name_native": "大红袍",
"class": "oolong",
"water_temp_c": "95",
"relations": [
{ "type": "grown_in", "kind": "region",
"slug": "wuyi-shan", "name": "Wuyi Shan" }
]
}
]
}Paging
total counts the whole category and subjects is this page of it. Keep going until offset + subjects.length >= total. The most common way to import a wiki badly is to read the first hundred rows and call it the dataset.
Good to know
- Every subject has
slug,name,kindandurl. Every other key is that kind's own fields, named exactly as they are on the site's edit form. When a field is added to the form, it shows up here too.?limit=1is the quickest way to see what a category carries right now. - Values are strings, as typed.
water_temp_cis"95", not95, because a contributor might write"90–95"or"just off the boil", and we'd rather not invent a number. relationsare outbound only. A row lists what its own page claims, such as a tea saying where it's grown. The reverse edges are on the rows that wrote them, in the same response.
03
GET /api/v1/festivals
Every tea festival we have dates for. Each row is an edition, like the 2026 Tokyo Tea Festival, not the festival as a whole. Editions are what have dates, a venue and a ticket link, so they're what you can put in a list, on a map or in a calendar.
Requests
GET https://wikiteadia.org/api/v1/festivals
GET https://wikiteadia.org/api/v1/festivals?year=2026
GET https://wikiteadia.org/api/v1/festivals?country=portugal&format=in_personParameters
| Name | Shape | Default |
|---|---|---|
year | a four-digit year: 2026 | every year |
country | a lowercase hyphenated name: portugal, united-states | all countries |
format | in_person, online or hybrid | all formats |
series | a series_slug, for every year of one festival | all festivals |
bbox | w,s,e,n in degrees | everywhere |
limit | 0 to 500 | 200 |
offset | any non-negative integer | 0 |
Leave out year and you get every year, past and future. That's the opposite of the website, which shows one year at a time, because an import usually wants the lot.
Heads up: country is a name here but an ISO code on /api/v1/places. That's a wart, not a design. Festival listings arrive with country names and we haven't mapped them yet. When we do, it'll be a new field, so nothing you've built breaks.
Response
{
"festivals": [
{
"slug": "berlin-tea-festival-2026-11-07",
"name": "Berlin Tea Festival",
"url": "https://wikiteadia.org/tea-festivals/…",
"start_date": "2026-11-07",
"end_date": "2026-11-08",
"timezone": "Europe/Berlin",
"series_slug": "berlin-tea-festival",
"status": "announced",
"format": "in_person",
"venue": "silent green Kulturquartier",
"address": "Gerichtstraße 35, 13347 Berlin",
"country": "germany",
"lat": 52.54569,
"lng": 13.36649,
"geo_precision": "surveyed"
}
],
"total": 52,
"limit": 200,
"offset": 0
}Paging
Keep going until you get an empty array. A short page isn't necessarily the last one. Results are ordered by start_date, then slug, so festivals that share a weekend never get skipped or repeated between pages.
Good to know
- Dates are calendar dates, not timestamps.
2026-11-07means the seventh of November in Berlin.timezonesays whose calendar that is. Store them as dates; turning them into instants invents a time we never published. - Check
geo_precisionbefore drawing a pin. Coordinates come as a pair or not at all, and the precision is one ofsurveyed,geocoded,village,countyorregion. The middle of a county and a tea house's front door are both just two numbers. - Festivals without coordinates are included. An online festival has no location, and a new one may not be geocoded yet. They're still real festivals with real dates. The GeoJSON feed is the one that leaves them out.
festivalsis not a category and won't become one. Likeplaces, it has its own shape.
04
Map and calendar feeds
Each year's festivals as a GeoJSON FeatureCollection, one Point per edition, and again as a calendar you can subscribe to.
Requests
GET https://wikiteadia.org/tea-festivals-2026.geojson
GET https://wikiteadia.org/tea-festivals-2026.geojson?bbox=-10,36,-6,42
GET https://wikiteadia.org/tea-festivals-2026.icsGood to know
- Unplaced editions are dropped from the GeoJSON rather than put at (0, 0). Use
/api/v1/festivalsif you want every record; this one is made for maps. bboxcan cross the antimeridian.?bbox=170,-20,-170,-10is twenty degrees of Pacific, not three hundred and forty.- Calendar events keep their UID between updates, so a subscriber's calendar updates an event instead of adding a duplicate.
These feeds are older than /api/v1 and aren't versioned, so they're the one thing here that might move. If they do, they'll move into /api/v1 and these URLs will keep working.
Caching and credit
Everything in these responses was written by people who want it to be reused. In return, we'd ask three things.
- Link back
- To the
urlfield on each record. That's where your readers can fix a mistake, and those fixes are what keep the data worth using. - Cache
- An hour for the
/api/v1/endpoints, five minutes for the GeoJSON. Fetching on every page view asks us a question you already know the answer to. It does mean a correction can take up to an hour to reach you, which is fine for an address and about the limit for opening times. - Say hello
- Tell us what you're building at hello@wikiteadia.org. You don't have to, and there's no key for us to revoke if you don't. It's just the only way we'll know to think of you before we change anything.
Not built yet
Things you might reasonably look for and not find, and where each one stands.
- Writes
- Read-only for now. Before we accept writes we need to work out how a reviewer handles an edit with no person behind it. Until then, the form on the site is the way in, and we're in no hurry to change that.
- Search
- An
/api/v1/searchmakes sense, but it needs a contract that holds still, which is a separate job from the search page. If you need it, ask. - Bulk export
- Paging through a category 500 rows at a time works today. A single download you can diff against last week's would be better for a mirror or an archive.
- Festival series as records
- You get a series' name, slug and website through its editions. For now, the only sign that a festival has ended is that it has no future editions, which is also what a year off looks like.
- Cancelled editions
- Left out everywhere, so to an importer a cancelled edition simply disappears. That's why
statusis in the payload: a future?status=filter can include them without changing the shape. - Region boundaries
- The obvious thing to serve as GeoJSON, and the one we can't serve until we know where each boundary came from. That's a licensing question, not an engineering one.
Something missing or wrong? Add it. The API serves what the wiki knows, so the fix is an edit, not a ticket.