For developers
KDE TO SOM API
Points of interest, trails and events for your own website. One GET, JSON
back. Nothing to install, nothing to run server-side.
Your first request
curl -H 'Origin: https://example.sk' \
'https://api2.kdetosom.sk/?tk=YOUR_TOKEN&lang=en&poifltr=[3]'
{
"meta": {
"lang": "en",
"numRows": 130,
"categoryName": "Cultural and historical sights",
"mediaBase": "https://kdetosom.sk/data/poi/",
"audioBase": "https://kdetosom.sk/data/poi/audio/",
"posterBase": "https://data.kdetosom.pro/posters/"
},
"data": [
{
"poi_id": 11989,
"poi_detail_id": 6965,
"poi_name": "Vartovka",
"description": "The Krupina watchtower was built around 1564…",
"lat": 48.3492767, "lng": 19.0791407,
"muni_name": "Krupina",
"img_name": "qx1gbDVMXoEbQ354PNAQwa95y", "img_ext": "jpg", "img_version": 8,
"has_audio": 1
}
]
}
Notice what is not in there: image URLs. See Images and audio — it is the one part of this API that surprises people, and the reason it works that way is that the files can move without your site changing.
Authentication
Every request carries a token: ?tk=YOUR_TOKEN. Two things gate it.
Your domain. A token is bound to the host it may be used from. Browsers
send Origin automatically; server-side calls must send it themselves.
Anything else gets 401 OriginNotAllowed.
Your monthly allowance, if your subscription has one. Over it,
406 QuotaExceeded. Unlimited subscriptions never see it.
The token is not a secret the way a password is — it sits in the source of a public page, and the domain binding is what protects it. Do not put it in a QR code or anywhere it can be lifted and reused from another host you also authorised.
Endpoints
One URL. What comes back is chosen with actp.
actp | returns |
|---|---|
omitted / wiam_pois | a list of points |
wiam_poi | one point |
wiam_tags | the category tree, with counts |
wiam_territories | district / county / region names |
wiam_groups | trails and audioguides you may show |
wiam_group | one trail, in order |
wiam_events | events — own section |
wiam_event_tags | the event category tree |
Listing points
| parameter | meaning |
|---|---|
lang | sk en de cz pl hu fr it es ru — Czech is cz, not cs |
poifltr | category ids as a JSON array: [3,6]. Omit for your default; [-1] means everything |
mncd dscd ccd rgcd | municipality / district / county / region code |
lat lng | the visitor's position — turns on distance sorting |
rds | radius in kilometres, not metres |
cnt | how many — on the distance-sorted path only |
limit | how many, on the unsorted path |
minsig | 1–4, minimum significance (see below) |
kinds | attraction, amenity, hospitality, descriptor |
Categories are unioned — poifltr=[3,6] is "3 or 6" — and
territory filters are intersected with them.
Send lat and lng and every row comes back with dist
in kilometres, nearest first. Without a position the list arrives in its stored order and
cnt is ignored, so that sites which page the list themselves receive all of
it; use limit if you want a cap.
Fields you can rely on
| Identity and text | poi_id poi_detail_id poi_name
description locality_desc url
lang version |
| Where it is | lat lng muni_name
muni_code district_code county_code
region_code is_city dist
show_address + the address block |
| Media | img_name img_ext img_version
has_custom_photo has_audio audio_version |
| Classification | tag_ids priority
img_source desc_source |
tag_ids is a comma-separated string of the categories a point carries — the
same ids wiam_tags returns. img_source and
desc_source are credits, not decoration: some photographs
may only be published with their author named. priority is significance,
1 (national) to 4 — the field a "worth travelling for" filter is built on.
dist only means something when you sent lat and
lng. v1 emits it either way, and without a position the value is
nonsense — point 3192 comes back as "1 km" from wherever you are. Here the field is
simply absent unless it was computed. If you are porting code that compensated for that,
delete the compensation rather than carrying it over.
poi_id identifies the place; poi_detail_id identifies
the text about it in one language and one wording. Link to a point by
poi_detail_id — that is what the detail endpoint takes.
Images and audio
The API returns the ingredients of a URL, never the URL. You assemble it:
const img = `${meta.mediaBase}${row.img_name}.${row.img_ext}?${row.img_version}`;
const audio = `${meta.audioBase}${row.poi_detail_id}.mp3`;
This is the contract, not an inconvenience. The files will move to a CDN; when they do,
mediaBase changes and your site keeps working. A hardcoded media host
is the one thing guaranteed to break. Read it from every response.
has_audio is not a boolean
0 | no recording |
1 | this row has its own — use its poi_detail_id |
| anything else | it is the poi_detail_id whose recording to play |
if (row.has_audio) {
const id = row.has_audio === 1 ? row.poi_detail_id : row.has_audio;
play(`${meta.audioBase}${id}.mp3`);
}
A place described in nine languages usually has one recording shared by several of them. That is what the third case is.
Categories, territories, trails, events
Categories (actp=wiam_tags) come back as a tree via
parent_id, with count and a slug that
never changes when a category is renamed — so it is safe to key off.
Names arrive already translated.
Territories (actp=wiam_territories) give
{districts, countys, regions}, each row {code, name, short, slug}.
Every point carries these codes, so this is how you label a filter without shipping your
own copy of the list.
Trails: actp=wiam_groups lists what you may show, then
actp=wiam_group&grpid=… returns the stops in curated order with
order_number_in_group. The order is the content — do not
re-sort it. Each stop appears once, in the language you asked for,
falling back to Slovak where a translation does not exist — a membership names a text
rather than a place, so a fully translated trail has ten memberships per stop.
Events
GET https://api2.kdetosom.sk/?actp=wiam_events&tk=…&lang=en&from=20260901&to=20261231&cnt=20
| parameter | meaning |
|---|---|
from to | YYYYMMDD. Default is today onwards — an event list is about what is still to come |
muni | municipality code |
region | region code |
importance | 1 = the region's headline events, higher = smaller |
cnt | how many |
Your subscription needs events enabled; without it the endpoint answers 401.
{
"id": 727,
"hashed_id": "6a685d4eae40dbdbee9f9b746727",
"name": "TAI CHI PRE ZDRAVIE",
"anotation": "Meditation in movement, for beginners…",
"date_start": "24.8.2026", "date_start_ts": 1787587200, "date_start_sort": "20260824",
"date_finish": "0000-00-00 00:00:00", "event_takes_more_days": 0,
"muni_code": 518387, "muni_name": "Dudince",
"district_code": 605, "county_code": 6, "region": 1, "region_name": "Región Hont",
"location_desc": "the meadow behind the spa", "location_poi_id": 163019,
"lat": "0.00000000", "lng": "0.00000000",
"event_importance": 3, "entry_fee": 0, "organizator": "Kúpele Dudince",
"tags": "902,3104",
"images": [{ "order": 1, "file": "6a685d…-668.jpg", "url": "https://…/posters/6a685d…-668.jpg" }]
}
Four things about event rows that will otherwise cost you an hour
Dates come in three forms and you need two of them.
date_start is ready to print (24.8.2026, no leading zeros —
display it verbatim), date_start_ts is a Unix timestamp to compute with, and
date_start_sort is YYYYMMDD for ordering.
date_finish is often 0000-00-00 00:00:00 — a
one-day event has no end. Its date_finish_ts is then
-62169984000, which is a real number and a nonsense date, so a naïve
comparison will happily place the event in the year 1. Check
event_takes_more_days before showing an end date at all; do not
test the timestamp.
lat and lng are frequently 0. Many
events are not at a mappable address. Use location_poi_id when it is set —
the event happens at a point of interest you can already link to — and
location_desc, which is free text somebody typed.
tags is a comma-separated string, not an array, and they are
event tag ids rather than POI categories: "902,3104". Split it and
resolve the names with wiam_event_tags.
Posters are in images as
{ order, file, url }, with url already assembled from
meta.posterBase. An event may have several and order is the
sequence somebody chose. The legacy poster_ext and
poster_version fields are usually empty — ignore them.
Link to an event by hashed_id. There is no single-event endpoint: a detail
page is composed from the list response. Ask if you need one.
An empty list out of season is normal — say "nothing coming up" rather
than showing an error.
Event categories
actp=wiam_event_tags returns a different tree from the POI one, on
four axes:
druh | what kind of thing it is — mandatory |
zaner | genre and theme |
publikum | who it is for |
prilezitost | the occasion |
An event may carry several tags across several axes. A row with
is_axis: 1 is a heading, not a value — check the flag, never
an id range, because the ranges change the day somebody adds an axis.
Significance and kind
Two questions that look like one.
kinds — what sort of thing it is:
attraction, amenity, hospitality,
descriptor.
minsig — how much it matters:
1 | national — people travel across the country for it |
2 | regional — worth a detour |
3 | local — worth a stop if you are passing |
4 | supplementary — a wayside cross, a memorial plaque |
minsig=2 means "1 and 2". A tourism site usually wants
kinds=attraction&minsig=3; a town map wants everything.
Errors
{ "error": { "type": "UnknownToken", "title": "Unauthorized",
"detail": "This token is not valid.", "status": 401 } }
400 | an actp we do not know, or a missing parameter |
401 | missing, malformed, unknown or inactive token; or a domain that is not yours |
404 | no such point or trail |
406 | monthly allowance exhausted |
501 | the cache is not built, or the endpoint is not implemented |
501 never means "no results". An unbuilt cache and an empty
answer are deliberately different responses, so a deployment problem cannot be mistaken
for a data one.
Caching
Responses are gzipped, application/vnd.api+json, with
Access-Control-Allow-Origin: *. The API answers from memory and a
whole-country list costs it under a millisecond, so you need not be shy with it. Even so,
cache by URL on your side: categories and territories change rarely (an hour is fine),
point lists rarely (five minutes). Data is refreshed when an editor changes something, so
an edit is visible within a minute or two rather than instantly.
Getting a token
Tokens are issued per website. Ask for one with the domain it will run on, the languages you need and whether you want events. If you are building for a customer, ask them to have their own domain added rather than reusing a token from another site — the domain binding is what keeps a token from being useful to whoever copies it.