Developers
The Wikiteadia API
Everything here is public, unauthenticated and open to every origin. There is no key, no quota, no registration and no terms page to click through.
That is not an oversight to be tidied up later — a reference work that meters reuse has stopped being a reference work — and it has a deliberate consequence: we cannot tell who is reading, which means we cannot break a shape once somebody depends on it. So the promises on this page are the whole contract. Everything not promised may change.
What is live
| 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 |
Caching is s-maxage — shared caches only, so a browser refetches while a CDN does not.
There is no search endpoint and no way to write. The site talks to itself over one other JSON URL, /search?format=json, which is deliberately not in that table and carries none of these promises: it is one of two representations of a page, it changes whenever that page does, and it will do so without notice.
GET /api/v1/placesEvery tea house and tea shop whose page has been published and which carries the seven facts a listing cannot be built without.
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=50Name Shape Default citya lowercase slug — lisbon, kyoto, hackney all cities countryISO 3166-1 alpha-2, either case all countries limit1–2000 500 A value that could not be a city slug or a country code is ignored, not rejected. An ignored filter returns everything, which is a wrong answer you can see and correct; a 500 is a wrong answer that looks like our fault.
{ "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" }slugis stable forever. It is the key another application's check-ins, favourites and bookmarks hang off, so renaming a place page is a breaking change to data that does not live here and that we cannot migrate.Eight fields are always present:
slug,name,lat,lng,city_slug,country_code,timezone,url. Every other field is absent rather than null when we do not know it, so a template cannot print the word “null” at a reader.hoursis derived,opening_hoursis the source. The raw string is what a contributor copied off the shop window, in OSM syntax, stored verbatim.hoursis that string parsed into[day, open, close]triples where day 0 is Sunday. When the string falls outside the subset the parser understands,hoursis absent andopening_hoursstill arrives — half a parsed week is not a smaller answer, it is a claim that a tea house is open on a day its own sign says shut.There is no
featured,rankorsponsoredfield, and there will not be one. Whether a tea house is worth a reader's evening is editorial judgement belonging to whoever publishes the guide — it is not a fact about the building, and a reference work that ranks its own subjects has started selling them. Rank your own listings.GET /api/v1/{category}Every published subject of one category, as the revision a reviewer approved.
{category}is the plural in the site's own URLs, so a page you can read is a page you can parse: /teas in a browser,/api/v1/teasin a script.teasregionsplacesproducerscultivarsprocessesbrewingtopicspeopleworksDo not hard-code that list. It is generated from the kind registry and grows when a kind is added — including on this page, which reads the same registry the router does. Ask for a category and read the 404 body if you guessed wrong:
{ "error": "unknown category", "categories": ["teas", "regions", "…"] }placesis the exception. It answers the bespoke contract above, not the shape below. It was written for a specific consumer before this endpoint existed, and a shape somebody already depends on does not get quietly replaced.Name Shape Default limit0–500 100 offsetany non-negative integer 0 Both are clamped, not rejected.
?limit=99999is somebody asking for everything, which is a reasonable thing to want and an unreasonable thing to serialise in one response; you get 500 rows and alimitof 500 in the body saying so.{ "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" } ] } ] }totalcounts the category;subjectsis this page. Page untiloffset + subjects.length >= total— and please check it, because the commonest way to import a wiki badly is to read the first hundred rows and call it the dataset.Every subject has
slug,name,kindandurl. Everything else may be absent. The other keys are that kind's own fields, under the same names the contributor filled in on the site's form — so a new field appears on the form, in the database and in this response on the same commit, and nothing internal is ever served by accident, because a key the form does not post is a key this endpoint does not know about.?limit=1is the fastest way to see what a category currently carries.Values are strings as typed.
water_temp_cis"95"and not95: the field is free text because a contributor may legitimately write"90–95"or"just off the boil", and coercing that to a number here would mean inventing one.relationsis outbound only — what this subject's own revision claims about the world. A region's page shows inbound edges too, because otherwise it would be blank; an API row is a record, and a record states what its own revision said. The inbound edges appear on the rows that wrote them, in the same response. There are no UUIDs anywhere in this API. To follow a relation, ask for that kind's category and join onslug.GET /api/v1/festivalsEvery tea festival this wiki has dates for. The resource is an edition, not a series. “Tokyo Tea Festival” is the series — a name, an organiser and a website, with no date and no venue. An edition is the 2026 one: the thing with dates, an address, coordinates and a ticket link, and therefore the thing an application can put in a list, on a map or in a calendar.
festivalsis not a category and never will be. The two festival kinds declare no browse path — they are read through the year pages — so this is its own endpoint with its own shape, the wayplacesis.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_personName Shape Default yeara four-digit year — 2026 every year countrya lowercase hyphenated country name — portugal, united-states all countries formatin_person, online or hybrid all formats seriesa series_slug — every year of one festival all festivals bboxw,s,e,n in degrees everywhere limit0–500 200 offsetany non-negative integer 0 No
yearmeans every year, past and future. The year pages exist because a person browsing wants one year at a time; an application importing festivals usually does not, so the default here is the opposite of the default there.countryis a name, not an ISO code, unlike/api/v1/places. That asymmetry is a wart rather than a design: festivals carry the country name the source listings publish, and nobody has yet done the work of mapping them onto ISO codes. So?country=ptfinds no festivals and?country=portugalfinds no places. Whichever way it is reconciled, it will be reconciled by adding a field, which is not a breaking change.{ "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 }Dates are wall dates, never instants.
2026-11-07is the seventh of November in Berlin, which is not a moment in time, and nothing in this endpoint converts it to one.timezoneis a label saying whose calendar those days belong to — apply it to the dates and you have invented a timestamp we did not publish. If you are storing these, store them as dates.Coordinates arrive together or not at all, and
geo_precisioncomes with them:surveyed,geocoded,village,countyorregion. It matters because a point is not self-describing — the centre of a county and the door of a tea house are both two numbers — and drawing the first as a street pin is a mistake this field lets you avoid.Unplaced editions are served, which is the difference between this and the GeoJSON feed. An online festival has no coordinates by nature and a newly-added one has not been geocoded yet; both are real festivals with real dates, and only a map has a reason to drop them.
Page until you get an empty array — not until you get fewer rows than you asked for, because a page can be short without being the last. Ordering is by
start_datethenslug, and the second half is load-bearing: festivals cluster on the same weekends, and paging through an order that does not break ties drops rows and repeats others.GET /tea-festivals-{year}.geojson·.icsA
FeatureCollectionof that year's editions, onePointper edition — and the same editions again as a subscribable calendar. Each calendar event'sUIDis stable across regenerations, so a subscriber's calendar updates an event rather than duplicating it.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.icsEditions with no coordinates are dropped rather than emitted at (0, 0), which is the reason
/api/v1/festivalsexists beside this: that one is shaped for records, this one for a map.bboxmay cross the antimeridian —?bbox=170,-20,-170,-10is ten degrees of Pacific, not three hundred and fifty.These predate
/api/v1and keep their URLs. They are not versioned, so they are the one shape here that could move; if they do, they will move to/api/v1and these paths will keep answering.
What is not live
- Writes
- Everything here is read-only. Adding a write means deciding how a reviewer sees a contribution that arrived without a person behind it. Until then the form on the site is the only way in, which is not a limitation we are in a hurry to remove.
- Search
- An
/api/v1/searchis a reasonable thing to want and a different thing to build, with a contract that holds still. Ask. - Bulk export
- Paging a category 500 rows at a time works and is what we would suggest today, but a single dump somebody can diff against last week's is a better thing for a mirror or an archive.
- Festival series as records
- You get a series' name, slug and website through its editions. To learn that a festival is defunct you currently have to notice it has no future editions — which is also what a festival taking a year off looks like.
- Cancelled editions
- Excluded everywhere, so an importer sees one disappear rather than being told it was called off. That is why
statusis in the payload at all: a future?status=can widen this without changing a shape. - Region boundaries
- The obvious thing to serve as GeoJSON, and the one dataset we cannot serve until we know where each boundary came from. Licensing, not engineering.
Reuse
Everything in these responses was contributed by people who expect it to be reused. Please:
- Link back
- To the
urlfield. It is where a reader corrects the record, and corrections are the only reason an open wiki is worth reading through an API at all. - Cache
- An hour for every
/api/v1/endpoint, five minutes for the GeoJSON. Refetching per page view asks the database a question you already have the answer to. A correction therefore takes up to an hour to reach you: tolerable for an address, and about the limit for an opening time. - Tell us what you are building
- hello@wikiteadia.org. Not a requirement, and there is no key to revoke if you do not. It is simply the only way we learn that a shape has a dependant before we consider changing it.
Something missing or wrong? Add it — the API serves what the wiki knows, so the fix is an edit rather than a ticket.