{"openapi":"3.1.0","info":{"title":"TravelType Public API","version":"1.0.0","description":"Read-only public API for discovering points of interest, cities, and countries — focused on the DACH region (Germany, Austria, Switzerland) with expanding international coverage — enriched with Wikidata/OSM identifiers and tag-based travel-style signals. No authentication required.","contact":{"name":"TravelType","url":"https://traveltype.app"},"license":{"name":"Proprietary; data CC-BY where derived from Wikidata/OSM"}},"servers":[{"url":"https://traveltype.app","description":"Production"}],"paths":{"/api/public/cities":{"get":{"operationId":"list_cities","summary":"List cities","description":"Returns active cities ordered by POI count, with coordinates and Wikidata QID. Paginate with limit/offset; filter by country with the ISO-3166-1 alpha-2 code. Use as a starting point for city-by-city exploration.","security":[],"parameters":[{"name":"country","in":"query","required":false,"description":"ISO 3166-1 alpha-2 country code filter.","schema":{"type":"string","example":"DE"}},{"name":"limit","in":"query","required":false,"schema":{"type":"integer","default":50,"minimum":1,"maximum":100,"example":50}},{"name":"offset","in":"query","required":false,"schema":{"type":"integer","default":0,"minimum":0,"example":0}}],"responses":{"200":{"description":"Page of cities","content":{"application/json":{"schema":{"$ref":"#/components/schemas/CityList"}}}},"429":{"description":"Rate limit exceeded (60 req/min per IP)","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"500":{"description":"Internal error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}}},"/api/public/cities/{slug}":{"get":{"operationId":"get_city","summary":"Get city details","description":"Returns one city by slug, including its POI count, top distinctive tags, and a sample of its top POIs.","security":[],"parameters":[{"name":"slug","in":"path","required":true,"schema":{"type":"string","example":"berlin"}},{"name":"lang","in":"query","required":false,"description":"Response language for human-readable name/description fields. EN is canonical; falls back across en/de when a translation is missing.","schema":{"type":"string","enum":["en","de"],"default":"en"}}],"responses":{"200":{"description":"City detail","content":{"application/json":{"schema":{"$ref":"#/components/schemas/CityDetail"}}}},"404":{"description":"Resource not found","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"429":{"description":"Rate limit exceeded (60 req/min per IP)","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"500":{"description":"Internal error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}}},"/api/public/countries":{"get":{"operationId":"list_countries","summary":"List countries","description":"Returns every country that has at least one active city, with city and POI counts.","security":[],"parameters":[],"responses":{"200":{"description":"List of countries","content":{"application/json":{"schema":{"type":"array","items":{"$ref":"#/components/schemas/CountrySummary"}}}}},"429":{"description":"Rate limit exceeded (60 req/min per IP)","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"500":{"description":"Internal error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}}},"/api/public/countries/{iso2}":{"get":{"operationId":"get_country","summary":"Get country details","description":"Returns one country by ISO 3166-1 alpha-2 code, including its active cities (top 100 by POI count).","security":[],"parameters":[{"name":"iso2","in":"path","required":true,"schema":{"type":"string","example":"DE"}}],"responses":{"200":{"description":"Country detail","content":{"application/json":{"schema":{"$ref":"#/components/schemas/CountryDetail"}}}},"404":{"description":"Resource not found","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"429":{"description":"Rate limit exceeded (60 req/min per IP)","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"500":{"description":"Internal error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}}},"/api/public/pois/search":{"get":{"operationId":"search_pois","summary":"Search points of interest","description":"Free-text POI search with prefix/tag/fuzzy ranking plus popularity and geo boosts. Provide `q` (free text) and/or `city` (slug); with both, the search is scoped to that city. Optionally narrow with `tags` (all must match). Returns up to 50 POIs.","security":[],"parameters":[{"name":"q","in":"query","required":false,"description":"Free-text query (min 2 chars).","schema":{"type":"string","example":"rooftop bar berlin"}},{"name":"city","in":"query","required":false,"description":"Restrict to a city slug.","schema":{"type":"string","example":"berlin"}},{"name":"tags","in":"query","required":false,"description":"Comma-separated tag slugs; a POI must carry all of them.","schema":{"type":"string","example":"rooftop,cocktails"}},{"name":"limit","in":"query","required":false,"schema":{"type":"integer","default":20,"minimum":1,"maximum":50,"example":20}},{"name":"lang","in":"query","required":false,"description":"Response language for human-readable name/description fields. EN is canonical; falls back across en/de when a translation is missing.","schema":{"type":"string","enum":["en","de"],"default":"en"}}],"responses":{"200":{"description":"Matching POIs","content":{"application/json":{"schema":{"$ref":"#/components/schemas/PoiList"}}}},"422":{"description":"Invalid request parameters","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ValidationError"}}}},"429":{"description":"Rate limit exceeded (60 req/min per IP)","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"500":{"description":"Internal error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}}},"/api/public/pois/{id}":{"get":{"operationId":"get_poi_details","summary":"Get POI details","description":"Returns one POI by UUID or slug, with full enrichment: coordinates, approved tags, Wikidata/OSM/Google identifiers, and hero image.","security":[],"parameters":[{"name":"id","in":"path","required":true,"description":"POI UUID or slug.","schema":{"type":"string","example":"brandenburger-tor"}},{"name":"lang","in":"query","required":false,"description":"Response language for human-readable name/description fields. EN is canonical; falls back across en/de when a translation is missing.","schema":{"type":"string","enum":["en","de"],"default":"en"}}],"responses":{"200":{"description":"POI detail","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Poi"}}}},"404":{"description":"Resource not found","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"429":{"description":"Rate limit exceeded (60 req/min per IP)","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"500":{"description":"Internal error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}}},"/api/public/pois/{id}/similar":{"get":{"operationId":"find_similar_pois","summary":"Find similar POIs","description":"Returns the nearest POIs to the given one by tag-cosine similarity (shared distinctive tags). Useful for 'more like this' discovery. Empty when the POI has only generic tags.","security":[],"parameters":[{"name":"id","in":"path","required":true,"description":"POI UUID or slug.","schema":{"type":"string","example":"brandenburger-tor"}},{"name":"limit","in":"query","required":false,"schema":{"type":"integer","default":10,"minimum":1,"maximum":30,"example":10}},{"name":"lang","in":"query","required":false,"description":"Response language for human-readable name/description fields. EN is canonical; falls back across en/de when a translation is missing.","schema":{"type":"string","enum":["en","de"],"default":"en"}}],"responses":{"200":{"description":"Similar POIs","content":{"application/json":{"schema":{"$ref":"#/components/schemas/PoiList"}}}},"404":{"description":"Resource not found","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"429":{"description":"Rate limit exceeded (60 req/min per IP)","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"500":{"description":"Internal error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}}}},"components":{"schemas":{"Poi":{"type":"object","required":["id","slug","name","lat","lng","tags"],"properties":{"id":{"type":"string","format":"uuid","example":"6f1c0b2e-3a4d-4b5e-8c7f-9a0b1c2d3e4f"},"slug":{"type":"string","example":"brandenburger-tor"},"name":{"type":"string","example":"Brandenburg Gate"},"description":{"type":["string","null"],"example":"An 18th-century neoclassical monument and one of Berlin's best-known landmarks."},"city_slug":{"type":["string","null"],"example":"berlin"},"country_iso2":{"type":["string","null"],"example":"DE"},"lat":{"type":"number","format":"double","example":52.5163},"lng":{"type":"number","format":"double","example":13.3777},"tags":{"type":"array","items":{"type":"string"},"maxItems":8,"description":"Up to 8 most distinctive approved tag slugs (sensitive categories excluded).","example":["landmark","architecture","neoclassical"]},"categories":{"type":"array","description":"Coarse groupings the POI belongs to, with localized label.","items":{"type":"object","required":["slug","label"],"properties":{"slug":{"type":"string","example":"sights-landmarks"},"label":{"type":"string","example":"Sights & Landmarks"}}}},"wikidata_qid":{"type":["string","null"],"example":"Q82425"},"osm_id":{"type":["string","null"],"description":"OSM reference in `type/id` form.","example":"way/518071791"},"google_place_id":{"type":["string","null"],"example":"ChIJiQnyXMdRqEcRY6IO0wYKxFY"},"image_url":{"type":["string","null"],"description":"Absolute URL to the locally-hosted hero image.","example":"https://traveltype.app/poi-images/brandenburger-tor.jpg"},"image_source":{"type":["string","null"],"enum":["wikimedia","google","user",null],"example":"wikimedia"},"url":{"type":"string","example":"https://traveltype.app/poi/brandenburger-tor"}}},"CitySummary":{"type":"object","required":["slug","name","country_iso2","lat","lng","poi_count"],"properties":{"slug":{"type":"string","example":"berlin"},"name":{"type":"string","example":"Berlin"},"name_en":{"type":["string","null"],"example":"Berlin"},"country_iso2":{"type":"string","example":"DE"},"lat":{"type":"number","format":"double","example":52.52},"lng":{"type":"number","format":"double","example":13.405},"poi_count":{"type":"integer","example":1854},"wikidata_qid":{"type":["string","null"],"example":"Q64"}}},"CityDetail":{"allOf":[{"$ref":"#/components/schemas/CitySummary"},{"type":"object","properties":{"top_tags":{"type":"array","items":{"type":"string"},"example":["nightlife","techno","street-art","museum"]},"top_categories":{"type":"array","items":{"type":"object","required":["slug","label"],"properties":{"slug":{"type":"string","example":"nightlife-bars"},"label":{"type":"string","example":"Nightlife & Bars"}}}},"sample_pois":{"type":"array","items":{"$ref":"#/components/schemas/Poi"}}}}]},"CountrySummary":{"type":"object","required":["iso2","name_de","name_en","city_count","poi_count"],"properties":{"iso2":{"type":"string","example":"DE"},"name_de":{"type":"string","example":"Deutschland"},"name_en":{"type":"string","example":"Germany"},"name_local":{"type":["string","null"],"example":"Deutschland"},"wikidata_qid":{"type":["string","null"],"example":"Q183"},"city_count":{"type":"integer","example":14},"poi_count":{"type":"integer","example":9213}}},"CountryDetail":{"allOf":[{"$ref":"#/components/schemas/CountrySummary"},{"type":"object","properties":{"cities":{"type":"array","items":{"$ref":"#/components/schemas/CitySummary"}}}}]},"CityList":{"type":"object","required":["cities","total","limit","offset"],"properties":{"cities":{"type":"array","items":{"$ref":"#/components/schemas/CitySummary"}},"total":{"type":"integer","example":142},"limit":{"type":"integer","example":50},"offset":{"type":"integer","example":0}}},"PoiList":{"type":"object","required":["pois","count"],"properties":{"pois":{"type":"array","items":{"$ref":"#/components/schemas/Poi"}},"count":{"type":"integer","example":12}}},"Error":{"type":"object","required":["error","message"],"properties":{"error":{"type":"string","example":"not_found"},"message":{"type":"string","example":"POI with id 'xyz' not found"},"trace_id":{"type":"string","example":"b1d3…"}}},"ValidationError":{"type":"object","required":["error","issues"],"properties":{"error":{"type":"string","example":"validation"},"issues":{"type":"array","items":{"type":"object","properties":{"path":{"type":"array","items":{"type":"string"},"example":["q"]},"message":{"type":"string","example":"Provide `q` or `city`."}}}},"trace_id":{"type":"string","example":"b1d3…"}}}}}}