Skip to content

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=lisbon

Ground 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.
slug is 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/v1 is covered by this page. /search?format=json is not: the site uses it internally and it changes whenever the search page does.

All endpoints

EndpointReturnsCache
GET /api/v1/placesTea houses and tea shops, with coordinates1 hour
GET /api/v1/{category}Every published subject of one category1 hour
GET /api/v1/festivalsTea festivals, with dates, venues and coordinates1 hour
GET /tea-festivals-{year}.geojsonThat year's festival editions as GeoJSON5 minutes
GET /tea-festivals-{year}.icsThe same editions as a calendar feed1 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=50

Parameters

NameShapeDefault
citya lowercase slug: lisbon, kyoto, hackneyall cities
countryISO 3166-1 alpha-2, either caseall countries
limit1 to 2000500

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, timezone and url. The rest appear when we know them.
  • opening_hours is the source, hours is our parse of it. The raw string is what a contributor copied off the shop window, in OSM syntax. hours turns it into [day, open, close] triples, with day 0 as Sunday. If we can't parse the whole week, we leave hours out rather than guess, and you still get the raw string.
  • No ranking, ever. There's no featured, rank or sponsored field. 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

NameShapeDefault
limit0 to 500100
offsetany non-negative integer0

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, kind and url. 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=1 is the quickest way to see what a category carries right now.
  • Values are strings, as typed. water_temp_c is "95", not 95, because a contributor might write "90–95" or "just off the boil", and we'd rather not invent a number.
  • relations are 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_person

Parameters

NameShapeDefault
yeara four-digit year: 2026every year
countrya lowercase hyphenated name: portugal, united-statesall countries
formatin_person, online or hybridall formats
seriesa series_slug, for every year of one festivalall festivals
bboxw,s,e,n in degreeseverywhere
limit0 to 500200
offsetany non-negative integer0

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-07 means the seventh of November in Berlin. timezone says whose calendar that is. Store them as dates; turning them into instants invents a time we never published.
  • Check geo_precision before drawing a pin. Coordinates come as a pair or not at all, and the precision is one of surveyed, geocoded, village, county or region. 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.
  • festivals is not a category and won't become one. Like places, 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.ics

Good to know

  • Unplaced editions are dropped from the GeoJSON rather than put at (0, 0). Use /api/v1/festivals if you want every record; this one is made for maps.
  • bbox can cross the antimeridian. ?bbox=170,-20,-170,-10 is 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 url field 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/search makes 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 status is 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.