{"ok":true,"api_version":"ramowki-ai/1","request_id":"rq_mv5vprd4823d6de896","base_url":"https://tvaggregator.pl","data":{"name":"tvaggregator.pl — Ramówki TV — API for AI models","api_version":"ramowki-ai/1","policy_version":"2026-10-11","base_url":"https://tvaggregator.pl","description":"Searchable archive of historical TV programme schedules (mostly Polish channels plus foreign channels printed in Polish press listings). Built for dating home VHS recordings, archival research and fact checking. AI models and automated clients must identify themselves and declare the purpose of EVERY request; the server approves or rejects each request.","database":{"coverage":{"total_entries":9752793,"channel_count":1915,"date_from":"1953-05-03","date_to":"2026-04-19"},"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)."]},"identification":{"rule":"Required on every request except the open documentation (this document, openapi.json, llms.txt). The server approves each request individually.","fields":[{"name":"agent","required":true,"max_length":100,"description":"Name (and version) of your AI agent, assistant or tool, e.g. \"Claude Code\" or \"MyArchiveBot/2.1\". Non-AI scripts give their tool name, e.g. \"python-requests-script/1.0\"."},{"name":"model","required":true,"max_length":100,"description":"Identifier of the AI model sending the request, e.g. \"claude-opus-5-5\". Non-AI scripts give their tool name and version."},{"name":"operator","required":false,"max_length":200,"description":"Optional: the person or organisation on whose behalf you act (name, handle, e-mail or URL)."},{"name":"purpose_category","required":true,"description":"Why you need THIS request — one of the allowed purpose categories."},{"name":"purpose","required":true,"min_length":20,"max_length":600,"description":"Free text: the concrete goal of THIS request (at least 4 words; at least 40 characters with category \"other\"), e.g. \"Dating VHS tape no. 12: when did TVP 2 broadcast the film X?\". It is logged and shown to the site administrator."}],"transports":{"query":"agent, model, operator, purpose_category, purpose as query parameters (works with URL-only fetch tools).","json_body":"POST: the same names as top-level JSON fields, or nested {\"identification\": {\"agent\", \"model\", \"operator\"}, \"purpose\": {\"category\", \"description\"}}.","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) values containing non-ASCII characters, e.g. Polish letters.","precedence":"For each field the first non-empty value wins: JSON body, then query string, then headers."},"approval_checks":["agent and model present, 2-100 characters, not placeholders (\"unknown\", \"test\", \"ai\", \"bot\", \"<model>\"...) and not the documentation example values.","purpose_category is one of the allowed categories; prohibited categories are rejected.","purpose is 20-600 characters, at least 4 words, not filler text, at least 40 characters for \"other\", and does not describe a prohibited use (training AI models on the data, scraping or exporting the whole database, reselling the data).","The operation must be allowed for the category (crosscheck/batch only for vhs_tape_dating, archival_research, content_identification).","The agent/model/operator is not on the administrator blocklist, and rate limits are respected."],"logging":"Every request is logged with its identification, purpose, decision and result count; the log is reviewed by the site administrator."},"purpose_categories":{"allowed":[{"id":"vhs_tape_dating","description":"Dating home VHS recordings: finding when the programmes recorded on a tape were broadcast (crosscheck).","operations":["approval","meta","channels","search","schedule","crosscheck","crosscheck_batch"]},{"id":"archival_research","description":"Research into the history of television programming and broadcast schedules.","operations":["approval","meta","channels","search","schedule","crosscheck","crosscheck_batch"]},{"id":"content_identification","description":"Identifying a programme, film or episode seen in a recording, clip or photo.","operations":["approval","meta","channels","search","schedule","crosscheck","crosscheck_batch"]},{"id":"fact_checking","description":"Verifying when, where or whether something was broadcast.","operations":["approval","meta","channels","search","schedule","crosscheck"]},{"id":"journalism","description":"Reporting or writing an article that needs verified broadcast facts.","operations":["approval","meta","channels","search","schedule","crosscheck"]},{"id":"education","description":"Teaching or learning materials about media and television history.","operations":["approval","meta","channels","search","schedule","crosscheck"]},{"id":"personal_interest","description":"A personal or nostalgic question about past TV programmes.","operations":["approval","meta","channels","search","schedule","crosscheck"]},{"id":"other","description":"Any other legitimate, specific purpose — describe it in at least 40 characters.","operations":["approval","meta","channels","search","schedule","crosscheck"]}],"prohibited":[{"id":"model_training","reason":"Using the schedule data to train, fine-tune or evaluate AI models is not permitted."},{"id":"bulk_export","reason":"Bulk export or mirroring of the database is not permitted — query only what a concrete task needs."},{"id":"scraping","reason":"Systematic scraping or crawling of the database is not permitted."},{"id":"commercial_resale","reason":"Reselling or commercially redistributing the data is not permitted."},{"id":"spam","reason":"Spam and automated abuse are not permitted."}]},"limits":{"requests_per_minute_per_ip":30,"requests_per_hour_per_identity":600,"crosscheck_per_minute_per_ip":12,"batch_per_minute_per_ip":4,"max_search_results_reachable":2000,"crosscheck_items_per_request":10,"batch_tapes_per_request":100},"endpoints":[{"method":"GET","path":"/ramowki/api/ai/v1","identification":false,"description":"This discovery document."},{"method":"GET","path":"/ramowki/api/ai/v1/openapi.json","identification":false,"description":"OpenAPI 3.1 specification for tool-calling clients."},{"method":"GET","path":"/ramowki/llms.txt","identification":false,"description":"Plain-language guide in Markdown (llms.txt)."},{"method":"GET, POST","path":"/ramowki/api/ai/v1/approval","identification":true,"operation":"approval","description":"Preflight: validates identification and purpose without querying data; returns the operations allowed for your purpose_category."},{"method":"GET","path":"/ramowki/api/ai/v1/meta","identification":true,"operation":"meta","description":"Coverage of the database: entry count, date range, channel count, largest channels."},{"method":"GET","path":"/ramowki/api/ai/v1/channels","identification":true,"operation":"channels","params":[{"name":"q","type":"string","max_length":100,"description":"Fragment of the channel name (case- and diacritic-insensitive). Omit to list the largest channels."},{"name":"limit","type":"integer","minimum":1,"maximum":500,"default":50,"description":"Maximum channels returned."}],"description":"Resolve exact channel names, with entry counts and first/last dates."},{"method":"GET, POST","path":"/ramowki/api/ai/v1/search","identification":true,"operation":"search","params":[{"name":"q","type":"string","max_length":300,"description":"Programme text. Words are AND-ed; use \"double quotes\" or (parentheses) for an exact phrase."},{"name":"channel","type":"string","max_length":100,"description":"Exact channel name from /channels, e.g. \"TVP 1\"."},{"name":"channel_contains","type":"string","max_length":100,"description":"Case-insensitive fragment of the channel name, e.g. \"tvp\". Ignored when \"channel\" is set."},{"name":"date_from","type":"string","format":"date","description":"YYYY-MM-DD, inclusive."},{"name":"date_to","type":"string","format":"date","description":"YYYY-MM-DD, inclusive."},{"name":"time_from","type":"string","pattern":"^([01][0-9]|2[0-3]):[0-5][0-9]$","description":"HH:MM (00:00-23:59), inclusive."},{"name":"time_to","type":"string","pattern":"^([01][0-9]|2[0-3]):[0-5][0-9]$","description":"HH:MM (00:00-23:59), inclusive."},{"name":"page","type":"integer","minimum":1,"default":1,"description":"Page number; page x limit may not exceed 2000."},{"name":"limit","type":"integer","minimum":1,"maximum":100,"default":50,"description":"Entries per page."},{"name":"format","type":"string","enum":["json","markdown"],"default":"json","description":"GET only: \"markdown\" returns a compact Markdown table instead of JSON."}],"description":"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."},{"method":"GET","path":"/ramowki/api/ai/v1/schedule","identification":true,"operation":"schedule","params":[{"name":"channel","type":"string","required":true,"max_length":100,"description":"Exact channel name from /channels."},{"name":"date","type":"string","format":"date","required":true,"description":"Broadcast day, YYYY-MM-DD."},{"name":"page","type":"integer","minimum":1,"default":1,"description":"Page number."},{"name":"limit","type":"integer","minimum":1,"maximum":500,"default":200,"description":"Entries per page (a day rarely exceeds 200)."},{"name":"format","type":"string","enum":["json","markdown"],"default":"json","description":"\"markdown\" returns a Markdown table."}],"description":"Full programme of one channel on one broadcast day."},{"method":"GET, POST","path":"/ramowki/api/ai/v1/crosscheck","identification":true,"operation":"crosscheck","params":[{"name":"items","type":"array","required":true,"description":"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."},{"name":"date_from","type":"string","format":"date","description":"YYYY-MM-DD lower bound for broadcasts (recommended, e.g. when VHS recording was possible)."},{"name":"date_to","type":"string","format":"date","description":"YYYY-MM-DD upper bound."},{"name":"max_gap_days","type":"number","minimum":0,"maximum":36500,"description":"Combinations spanning more days are marked within_max_gap=false and ranked last; 0 = no limit."},{"name":"require_order","type":"boolean","default":false,"description":"Only combinations whose broadcast order matches the item order."},{"name":"channel_contains","type":"string","max_length":100,"description":"Restrict all items to channels containing this fragment."},{"name":"top_k","type":"integer","minimum":1,"maximum":10,"default":10,"description":"Number of combinations returned (batch default 3)."},{"name":"format","type":"string","enum":["json","markdown"],"default":"json","description":"GET only: \"markdown\" output."}],"description":"VHS tape dating: for 1-10 titles recorded on one tape, find the combinations of broadcasts (one per title) with the smallest time span."},{"method":"POST","path":"/ramowki/api/ai/v1/crosscheck/batch","identification":true,"operation":"crosscheck_batch","params":[{"name":"tapes","type":"array","required":true,"description":"1-100 tapes: [{label: \"553\", items: [... same item format as /crosscheck, max 10 ...]}]. Polish aliases accepted: kasety / kaseta / pozycje / tytul / warianty."},{"name":"date_from","type":"string","format":"date","description":"YYYY-MM-DD lower bound for broadcasts (recommended, e.g. when VHS recording was possible)."},{"name":"date_to","type":"string","format":"date","description":"YYYY-MM-DD upper bound."},{"name":"max_gap_days","type":"number","minimum":0,"maximum":36500,"description":"Combinations spanning more days are marked within_max_gap=false and ranked last; 0 = no limit."},{"name":"require_order","type":"boolean","default":false,"description":"Only combinations whose broadcast order matches the item order."},{"name":"channel_contains","type":"string","max_length":100,"description":"Restrict all items to channels containing this fragment."},{"name":"top_k","type":"integer","minimum":1,"maximum":10,"default":10,"description":"Number of combinations returned (batch default 3)."}],"description":"Crosscheck for up to 100 tapes in one request. Only for purpose_category vhs_tape_dating, archival_research or content_identification."}],"response_envelope":{"success":"{ ok: true, api_version, request_id, base_url, approval: { status: \"approved\", agent, model, operator, purpose_category, purpose, policy_version, rate_limit }, data: {...}, notes: [...] }","error":"{ ok: false, api_version, request_id, base_url, error: { code, message, hint, field?, allowed_values?, example?, docs } }","links":"Links in responses are relative to base_url. next_page links omit identification — send agent, model, purpose_category and purpose again."},"error_codes":[{"code":"IDENTIFICATION_REQUIRED","http_status":400,"meaning":"agent and/or model missing."},{"code":"INVALID_IDENTIFICATION","http_status":400,"meaning":"agent/model/operator has a bad length, is a placeholder or the documentation example."},{"code":"PURPOSE_REQUIRED","http_status":400,"meaning":"purpose or purpose_category missing."},{"code":"INVALID_PURPOSE_CATEGORY","http_status":400,"meaning":"Unknown purpose_category (see allowed_values)."},{"code":"PURPOSE_TOO_VAGUE","http_status":400,"meaning":"purpose too short, too few words, filler text or copied from the documentation."},{"code":"PURPOSE_TOO_LONG","http_status":400,"meaning":"purpose longer than 600 characters."},{"code":"PURPOSE_NOT_ALLOWED","http_status":403,"meaning":"Prohibited category or prohibited intent (model training, bulk export, scraping, resale)."},{"code":"PURPOSE_OPERATION_MISMATCH","http_status":403,"meaning":"The operation is not available for the declared purpose_category."},{"code":"AGENT_BLOCKED","http_status":403,"meaning":"The agent, model or operator was blocked by the administrator."},{"code":"RATE_LIMITED","http_status":429,"meaning":"Too many requests; see Retry-After / retry_after_seconds."},{"code":"INVALID_PARAMETER","http_status":400,"meaning":"A parameter has a wrong format or value (see field)."},{"code":"MISSING_CRITERIA","http_status":400,"meaning":"Search needs q or a date bound (date_from / date_to); channel filters only narrow."},{"code":"PAGE_TOO_DEEP","http_status":400,"meaning":"page x limit exceeds 2000; narrow the query."},{"code":"UNKNOWN_CHANNEL","http_status":400,"meaning":"No such channel; see suggestions."},{"code":"INVALID_JSON","http_status":400,"meaning":"The request body is not valid JSON."},{"code":"PAYLOAD_TOO_LARGE","http_status":413,"meaning":"The request body is too large."},{"code":"INVALID_REQUEST","http_status":400,"meaning":"The request could not be processed."},{"code":"METHOD_NOT_ALLOWED","http_status":405,"meaning":"Wrong HTTP method for this endpoint."},{"code":"NOT_FOUND","http_status":404,"meaning":"Unknown endpoint."},{"code":"USE_AI_API","http_status":403,"meaning":"A web-interface endpoint was called by an automated client; use the AI API."},{"code":"DATABASE_UNAVAILABLE","http_status":503,"meaning":"The schedule database is temporarily unavailable."},{"code":"INTERNAL_ERROR","http_status":500,"meaning":"Unexpected server error; quote request_id when reporting."}],"examples":{"note":"The identification below is a documentation EXAMPLE and is rejected if copied — use your real agent name, model identifier and the concrete purpose of each request.","get_search":"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","get_crosscheck":"https://tvaggregator.pl/ramowki/api/ai/v1/crosscheck?item=Westerplatte&item=Zakazane+piosenki&date_from=1991-01-01&date_to=1996-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","headers":{"X-AI-Agent":"ArchiveHelper/1.0","X-AI-Model":"example-model-2026","X-AI-Operator":"jan.kowalski@example.com","X-AI-Purpose-Category":"vhs_tape_dating","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"},"post_crosscheck":{"url":"https://tvaggregator.pl/ramowki/api/ai/v1/crosscheck","json":{"agent":"ArchiveHelper/1.0","model":"example-model-2026","operator":"jan.kowalski@example.com","purpose_category":"vhs_tape_dating","purpose":"Dating VHS tape no. 553: finding when \"Concorde - port lotniczy 79\" and \"Fort Boyard\" were broadcast","items":[{"title":"Concorde - port lotniczy '79","variants":["Port lotniczy 79"]},"Fort Boyard"],"date_from":"1991-01-01","date_to":"1996-12-31","max_gap_days":180}},"post_batch":{"url":"https://tvaggregator.pl/ramowki/api/ai/v1/crosscheck/batch","json":{"agent":"ArchiveHelper/1.0","model":"example-model-2026","purpose_category":"vhs_tape_dating","purpose":"Dating VHS tape no. 553: finding when \"Concorde - port lotniczy 79\" and \"Fort Boyard\" were broadcast","tapes":[{"label":"553","items":["Concorde - port lotniczy 79","Fort Boyard"]},{"label":"788","items":["Westerplatte","Zakazane piosenki"]}],"date_from":"1991-01-01","date_to":"1996-12-31","max_gap_days":180}}},"links":{"openapi":"/ramowki/api/ai/v1/openapi.json","llms_txt":"/ramowki/llms.txt","web_ui":"/ramowki/"},"summary_pl":"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."}}