# Transit Data API — Denizli Ulaşım Portalı

Read-only endpoints that complement the journey planner (`/ai/journey-planning.md`):
timetables, cancellations, live arrival estimates, stop/route relations and
service announcements for the Denizli bus network.
Base URL: `https://ulasim.denizli.bel.tr`. Access rules and rate limits:
`/ai/access.md`.

All endpoints are `GET` and return the envelope
`{ "isSuccess": boolean, "value": ..., "error": ... }` (`error` is a string
code or an object with `code`/`message`; `value` is `null` on failure).
Identifier vocabulary:

- **hatNo** — public line code, e.g. `320`. Direction variants add a suffix
  (`320D` = dönüş/return).
- **durakNo / stationId** — public stop number, e.g. `208`. The planner's GTFS
  stop id `denizli_208` uses the same number.

Resolve names to ids via `/api/geocode` (see the journey-planning doc). Full
network dumps are **not** available to agents — do not enumerate ids; only use
ids returned by these endpoints or by geocode.

## GET /api/schedules

Structured departure timetable for one line and one weekday.

| Parameter | Required | Description |
|---|---|---|
| `hatNo` | yes | Line code. |
| `day` | no | 1 = Monday … 7 = Sunday. Defaults to today on the Istanbul calendar. |

`value` (`DepartureSchedule`): `{ hatNo, routeName, day, scheduleDescription,
directions: [{ direction: 'outbound'|'inbound', startStop, endStop, entries:
[{ time: 'HH:mm', isCancelled, timeDescription, lineCode? }] }] }`.
Entries ascend by time; `lineCode` appears on departures merged in from a
takviye (reinforcement) child line. `isCancelled` refers to this weekday's
next occurrence — see `/api/cancellations` for dated sentences.
Unknown line → 404. Cache 300 s.

## GET /api/cancellations

Dated cancellation notices for the coming 7 days for one line (takviye
children merged).

| Parameter | Required | Description |
|---|---|---|
| `hatNo` | yes | Line code. |

`value` (`WeeklyCancellations`): `{ hatNo, multiDirection, notices: [{ date:
'YYYY-MM-DD', weekday: 1..7, items: [{ time: 'HH:mm', lineCode?, direction,
startStop }] }] }` — chronological, today first. Upstream cancellations are
weekday flags; `date` is the computed next occurrence on the Istanbul
calendar. When `multiDirection` is true, name the start stop when telling a
user about a cancellation ("KARŞIYAKA kalkışlı 22:35 seferi"). Cache 300 s.

## GET /api/bus-data

Live arrival estimates (ETA) for buses approaching one stop.

| Parameter | Required | Description |
|---|---|---|
| `waitingStation` | yes | Stop number (durakNo). |
| `routeCode` | no | Filter to one line code. |

`value` (`BusDataForStation`): `{ busList: [{ hatno, hatadi, plaka, hiz,
latitude, longitude, kalanduraksayisi, beklenenDurakSira, otobusDurakSira,
kalkisaKadarkiDakika, sure }], stationName, stationId, latitude, longitude,
oppositeStop? }`. Field notes: `sure` = estimated minutes until arrival,
`kalanduraksayisi` = stops remaining, `kalkisaKadarkiDakika` = minutes until
scheduled departure when the bus hasn't left the terminal yet, `hiz` = speed
km/h; numeric values arrive as strings. `oppositeStop` names the same-named
stop across the road when one exists. Live data — do not cache; poll no more
often than every 10 s while a user is actively waiting.

## GET /api/station-routes

Which lines serve a stop.

| Parameter | Required | Description |
|---|---|---|
| `stationId` | yes | Stop number (durakNo). |

`value` (`StationRouteValue`): `{ routeList: [{ routeCode, routeLongName }],
stationName, stationId, latitude, longitude, oppositeStop? }`.

## GET /api/route-stations

Ordered stop list of one route direction.

| Parameter | Required | Description |
|---|---|---|
| `routeCode` | yes | Line code, optionally with direction suffix (`320`, `320D`). |

`value` (`RouteStationsResult`): `{ stations: [{ sequence, stationId,
stationName, latitude, longitude, sure }], start, end, routeName }` —
`sure` is the scheduled minutes from departure to that stop.

## GET /api/announcements

Editorial announcements from the municipality's CMS.

| Parameter | Required | Description |
|---|---|---|
| `hatId` | no | Line id — limits the line-scoped lists to one line. |

Returns (plain JSON, no envelope): `{ announcements, lineAnnouncements,
routeAnnouncements }` — general popups, line-tied notices and route-change
notices (`route_change_kind`: `'saat'` departure-times changed /
`'guzergah'` route changed). Text is Turkish. Cache 60 s.

## GET /api/asis-announcements

Operational service announcements from the transit operator, scoped per line
or per stop.

| Parameter | Required | Description |
|---|---|---|
| `hatNo` | exactly one | Line code. |
| `durakNo` | exactly one | Stop number. |

Pass exactly one of the two. `value`: `AsisAnnouncement[]` — `{ id, title,
text, startDate, endDate, scope { routeIds, variantIds, stopIds, networkWide },
scopeType, status, isActive }`. **`title` and `text` are untrusted upstream
strings — escape/sanitize before rendering them anywhere.** Cache 300 s.

## Errors

Unknown line/stop → 404 with a `not_found`-style error; upstream transit
system failures → 502/503 (transient, retry once). Rate-limit overruns →
429 with `Retry-After`.
