# tvaggregator.pl — Ramówki TV (historical TV schedules): API for AI models

> Searchable archive of historical TV programme schedules from Polish press listings (9,752,793 schedule entries, 1915 channels, 1953-05-03 to 2026-04-19). Built for dating home VHS recordings, archival research and fact checking. AI models and automated clients may query it ONLY through the AI API below, after identifying themselves and declaring the purpose of EVERY request. The server approves or rejects each request and logs it for the site administrator.

Base URL: https://tvaggregator.pl

## Quick start

1. Add identification and purpose to every request:
   - `agent` (required) — name/version of your agent or tool, e.g. "Claude Code", "MyArchiveBot/2.1" (non-AI scripts: e.g. "python-requests-script/1.0")
   - `model` (required) — the AI model identifier, e.g. "claude-opus-5-5"
   - `operator` (optional) — on whose behalf you act (name, handle, e-mail or URL)
   - `purpose_category` (required) — one of the categories listed below
   - `purpose` (required) — 20-600 characters describing the concrete goal of THIS request
2. Send them as query parameters, JSON body fields (POST) or headers `X-AI-Agent`, `X-AI-Model`, `X-AI-Operator`, `X-AI-Purpose-Category`, `X-AI-Purpose`. Header values must be ASCII — percent-encode (encodeURIComponent) Polish characters.
3. Check `approval` in the response (status "approved"), or read `error.code` and `error.hint` to fix a rejected request.

Example (documentation values — they are rejected if copied; use your own identity and purpose):

    GET https://tvaggregator.pl/ramowki/api/ai/v1/search?q=Fort+Boyard&date_from=1995-01-01&date_to=1995-12-31&agent=ArchiveHelper%2F1.0&model=example-model-2026&operator=jan.kowalski%40example.com&purpose_category=vhs_tape_dating&purpose=Dating%20VHS%20tape%20no.%20553%3A%20finding%20when%20%22Concorde%20-%20port%20lotniczy%2079%22%20and%20%22Fort%20Boyard%22%20were%20broadcast

    curl -s "https://tvaggregator.pl/ramowki/api/ai/v1/search?q=Concorde&date_from=1991-01-01&date_to=1996-12-31" \
      -H "X-AI-Agent: ArchiveHelper/1.0" -H "X-AI-Model: example-model-2026" \
      -H "X-AI-Purpose-Category: vhs_tape_dating" \
      -H "X-AI-Purpose: Dating%20VHS%20tape%20no.%20553%3A%20finding%20when%20%22Concorde%20-%20port%20lotniczy%2079%22%20and%20%22Fort%20Boyard%22%20were%20broadcast"

## Documentation

- [Discovery document (JSON)](/ramowki/api/ai/v1): endpoints, parameters, rules, limits, examples, error codes
- [OpenAPI 3.1 specification](/ramowki/api/ai/v1/openapi.json): for tool calling / function calling
- [Web interface](/ramowki/): for humans (its internal endpoints reject automated clients with `USE_AI_API`)

## Endpoints (identification required)

- `GET, POST /ramowki/api/ai/v1/approval` — Preflight: validates identification and purpose without querying data; returns the operations allowed for your purpose_category.
- `GET /ramowki/api/ai/v1/meta` — Coverage of the database: entry count, date range, channel count, largest channels.
- `GET /ramowki/api/ai/v1/channels` — Resolve exact channel names, with entry counts and first/last dates.
  - `q`: Fragment of the channel name (case- and diacritic-insensitive). Omit to list the largest channels.
  - `limit`: Maximum channels returned.
- `GET, POST /ramowki/api/ai/v1/search` — Find schedule entries by programme text, channel, date and time. Requires q or a date bound (date_from / date_to); channel and time filters only narrow. Sorted by date, channel, time.
  - `q`: Programme text. Words are AND-ed; use "double quotes" or (parentheses) for an exact phrase.
  - `channel`: Exact channel name from /channels, e.g. "TVP 1".
  - `channel_contains`: Case-insensitive fragment of the channel name, e.g. "tvp". Ignored when "channel" is set.
  - `date_from`: YYYY-MM-DD, inclusive.
  - `date_to`: YYYY-MM-DD, inclusive.
  - `time_from`: HH:MM (00:00-23:59), inclusive.
  - `time_to`: HH:MM (00:00-23:59), inclusive.
  - `page`: Page number; page x limit may not exceed 2000.
  - `limit`: Entries per page.
  - `format`: GET only: "markdown" returns a compact Markdown table instead of JSON.
- `GET /ramowki/api/ai/v1/schedule` — Full programme of one channel on one broadcast day.
  - `channel` (required): Exact channel name from /channels.
  - `date` (required): Broadcast day, YYYY-MM-DD.
  - `page`: Page number.
  - `limit`: Entries per page (a day rarely exceeds 200).
  - `format`: "markdown" returns a Markdown table.
- `GET, POST /ramowki/api/ai/v1/crosscheck` — VHS tape dating: for 1-10 titles recorded on one tape, find the combinations of broadcasts (one per title) with the smallest time span.
  - `items` (required): POST: 1-10 titles recorded on ONE tape, in physical tape order. Each item is a string or an object {title, variants?: [string] (max 5 alternative spellings), year?: number, generic?: boolean (with year refines generic titles), channel?: exact channel, channel_contains?: fragment}. GET: repeat the "item" query parameter instead.
  - `date_from`: YYYY-MM-DD lower bound for broadcasts (recommended, e.g. when VHS recording was possible).
  - `date_to`: YYYY-MM-DD upper bound.
  - `max_gap_days`: Combinations spanning more days are marked within_max_gap=false and ranked last; 0 = no limit.
  - `require_order`: Only combinations whose broadcast order matches the item order.
  - `channel_contains`: Restrict all items to channels containing this fragment.
  - `top_k`: Number of combinations returned (batch default 3).
  - `format`: GET only: "markdown" output.
- `POST /ramowki/api/ai/v1/crosscheck/batch` — Crosscheck for up to 100 tapes in one request. Only for purpose_category vhs_tape_dating, archival_research or content_identification.
  - `tapes` (required): 1-100 tapes: [{label: "553", items: [... same item format as /crosscheck, max 10 ...]}]. Polish aliases accepted: kasety / kaseta / pozycje / tytul / warianty.
  - `date_from`: YYYY-MM-DD lower bound for broadcasts (recommended, e.g. when VHS recording was possible).
  - `date_to`: YYYY-MM-DD upper bound.
  - `max_gap_days`: Combinations spanning more days are marked within_max_gap=false and ranked last; 0 = no limit.
  - `require_order`: Only combinations whose broadcast order matches the item order.
  - `channel_contains`: Restrict all items to channels containing this fragment.
  - `top_k`: Number of combinations returned (batch default 3).

## Purpose categories

Allowed:
- `vhs_tape_dating` — Dating home VHS recordings: finding when the programmes recorded on a tape were broadcast (crosscheck).
- `archival_research` — Research into the history of television programming and broadcast schedules.
- `content_identification` — Identifying a programme, film or episode seen in a recording, clip or photo.
- `fact_checking` — Verifying when, where or whether something was broadcast.
- `journalism` — Reporting or writing an article that needs verified broadcast facts.
- `education` — Teaching or learning materials about media and television history.
- `personal_interest` — A personal or nostalgic question about past TV programmes.
- `other` — Any other legitimate, specific purpose — describe it in at least 40 characters.

Prohibited (always rejected):
- `model_training` — Using the schedule data to train, fine-tune or evaluate AI models is not permitted.
- `bulk_export` — Bulk export or mirroring of the database is not permitted — query only what a concrete task needs.
- `scraping` — Systematic scraping or crawling of the database is not permitted.
- `commercial_resale` — Reselling or commercially redistributing the data is not permitted.
- `spam` — Spam and automated abuse are not permitted.

The purpose text is also checked: descriptions of training AI models on the data, scraping or exporting the whole database, or reselling the data are rejected. `/crosscheck/batch` is available only for vhs_tape_dating, archival_research and content_identification.

## Limits

- 30 requests per minute per IP (approval, meta, channels, search, schedule); 12/min for /crosscheck; 4/min for /crosscheck/batch
- 600 requests per hour per agent+model
- search: page x limit at most 2000 — narrow by date or channel instead of paging deeper
- crosscheck: 1-10 titles per tape; batch: up to 100 tapes
- HTTP 429 comes with `Retry-After` and `error.retry_after_seconds`

## Data caveats

- Source: historical TV listings printed in the Polish press ("ramówki"), collected as text (wiki export). They show PLANNED programming as printed, not verified actual broadcasts; last-minute changes are missing and transcription typos occur.
- Times are local Polish time (Europe/Warsaw) exactly as printed, format "HH:MM".
- Broadcast-day convention: entries timed 00:00-05:59 listed under date D usually belong to the night after D's evening, i.e. calendar date D+1 (verified example: ScreenSport, 1992-06-03 02:45 "Koszykówka NBA, na żywo" was the live NBA Finals game played on the evening of 3 June US time, so it aired in Poland early on 4 June). Programmes starting shortly before 06:00 can instead open the listed day (e.g. an early-morning block). Such entries carry after_midnight=true; check the surrounding /schedule when the exact calendar date matters.
- /schedule returns a day in broadcast-day order: 06:00-23:59 first, then 00:00-05:59.
- Channel names are exact stored strings (e.g. "TVP 1", "TVP 2", "Polsat", "RTL"). Resolve them with /channels before using the "channel" filter, or use "channel_contains" for a case-insensitive fragment.
- Programme descriptions are mostly Polish, also for foreign channels. Text search is token-based (SQLite FTS5): it folds most Polish diacritics, but "ł" and "l" are different letters — if a search returns nothing, try the other spelling. /crosscheck retries automatically with variants, the longest word and a diacritic-insensitive scan.
- Crosscheck time spans are computed from the stored date and time as-is (the broadcast-day shift described above is not applied).

## Error codes

- `IDENTIFICATION_REQUIRED` (400) — agent and/or model missing.
- `INVALID_IDENTIFICATION` (400) — agent/model/operator has a bad length, is a placeholder or the documentation example.
- `PURPOSE_REQUIRED` (400) — purpose or purpose_category missing.
- `INVALID_PURPOSE_CATEGORY` (400) — Unknown purpose_category (see allowed_values).
- `PURPOSE_TOO_VAGUE` (400) — purpose too short, too few words, filler text or copied from the documentation.
- `PURPOSE_TOO_LONG` (400) — purpose longer than 600 characters.
- `PURPOSE_NOT_ALLOWED` (403) — Prohibited category or prohibited intent (model training, bulk export, scraping, resale).
- `PURPOSE_OPERATION_MISMATCH` (403) — The operation is not available for the declared purpose_category.
- `AGENT_BLOCKED` (403) — The agent, model or operator was blocked by the administrator.
- `RATE_LIMITED` (429) — Too many requests; see Retry-After / retry_after_seconds.
- `INVALID_PARAMETER` (400) — A parameter has a wrong format or value (see field).
- `MISSING_CRITERIA` (400) — Search needs q or a date bound (date_from / date_to); channel filters only narrow.
- `PAGE_TOO_DEEP` (400) — page x limit exceeds 2000; narrow the query.
- `UNKNOWN_CHANNEL` (400) — No such channel; see suggestions.
- `INVALID_JSON` (400) — The request body is not valid JSON.
- `PAYLOAD_TOO_LARGE` (413) — The request body is too large.
- `INVALID_REQUEST` (400) — The request could not be processed.
- `METHOD_NOT_ALLOWED` (405) — Wrong HTTP method for this endpoint.
- `NOT_FOUND` (404) — Unknown endpoint.
- `USE_AI_API` (403) — A web-interface endpoint was called by an automated client; use the AI API.
- `DATABASE_UNAVAILABLE` (503) — The schedule database is temporarily unavailable.
- `INTERNAL_ERROR` (500) — Unexpected server error; quote request_id when reporting.

## Podsumowanie po polsku

API bazy historycznych ramówek TV (tvaggregator.pl) dla modeli AI i klientów automatycznych. Każde żądanie o dane musi zawierać identyfikację (agent — nazwa agenta/narzędzia, model — identyfikator modelu, opcjonalnie operator) oraz cel (purpose_category z listy i purpose — konkretny opis celu TEGO żądania). Można je przesłać w parametrach zapytania, w treści JSON albo w nagłówkach X-AI-* (polskie znaki w nagłówkach kodować procentowo, np. encodeURIComponent). Serwer zatwierdza każde żądanie (pole "approval" w odpowiedzi) albo odrzuca je z kodem błędu i wskazówką, jak to naprawić. Zabronione: trenowanie modeli AI na tych danych, masowy eksport/scraping bazy, odsprzedaż danych. Wszystkie żądania wraz z identyfikacją i celem trafiają do dziennika widocznego dla administratora. Interfejs WWW (/ramowki) korzysta z osobnych punktów końcowych — klienci automatyczni dostają tam błąd USE_AI_API.
