← Календарь

GCal API

Документация для интеграции (satya-dharma и другие клиенты). UI календаря — только QA-предпросмотр.

Base URL этого стенда:

OpenAPI 3.1 (JSON) — машиночитаемый контракт календарного ядра API и профиля Satya Dharma; текущая версия HTTP-контракта — 1.2.2.

Формат ответа

Большинство JSON-эндпоинтов:

// успех
{ "success": true, "data": { ... }, "message": "..."? }

// ошибка (часто HTTP 400)
{ "success": false, "error": "...", "error_code": "VALIDATION_ERROR", "error_details": ...? }

GET /health — исключение: { "status": "ok", "version": "..." } без envelope; поле version содержит build SHA. Версия всего приложения — 2.5.12 (SemVer), она отдельно показана в шапке QA-календаря.

Кэш API: заголовок Cache-Control: private, no-cache.

Ответы гzip-сжаты, если клиент шлёт Accept-Encoding: gzip (браузеры и большинство HTTP-клиентов делают это сами) — Content-Encoding: gzip в ответе, тело меньше в разы (~6× на /api/calendar-data). Ответы короче ~500 байт (напр. ошибки валидации) не сжимаются — оверхед gzip-заголовка не окупается.

Как передавать локацию

Для /api/calendar-data, /api/calendar-data/year, /api/calendar-data/next-event, /api/day-detail, /api/day-detail/astronomy и /api/day-detail/fasting-logic — два способа:

  1. geonames_id — любая запись каталога city_catalog (~170k городов + несколько кастомных записей вроде Kosshy/Mayapur, см. ниже; результат /api/locations/catalog);
  2. или lat + lon + timezone_offset вместе (рекомендуется внешним сайтам со своим списком городов).

Если передано и то, и другое — побеждает молча geonames_id. Невалидный/несуществующий geonames_idVALIDATION_ERROR, не тихая подстановка другого города. Часть координатной тройки → VALIDATION_ERROR. Ничего не передано → geonames_id по умолчанию (Алматы, 1526384).

Все четыре эндпоинта календаря (calendar-data, calendar-data/year, calendar-data/next-event, day-detail) и day-detail/fasting-logic дополнительно принимают caturmasya_system=purnima|pratipat|ekadasi — какая из трёх традиций расчёта границ месяцев Чатурмасьи используется. По умолчанию — purnima — дефолт самой gaurabda из коробки, и он же совпадает с vaisnavacalendar.info (эталон, с которым сверяется satya-dharma). Неизвестное значение → VALIDATION_ERROR. Значение, которое реально применилось, всегда есть в ответе как поле caturmasya_system.

Дефолт ненадолго переключали на pratipat — единственной проверкой тогда был Будапешт, где он побитово совпал с эталоном. На выборке из 202 городов × 2026 год это не подтвердилось: purnima совпадает с эталоном в 174/202 городов, pratipat — только в 11/202 (Будапешт — один из этих 11, не типичный случай). Дефолт вернули на purnima.

Те же эндпоинты принимают и fasting_note_style=classic|modern — формулировка заметок о посте при праздниках святых (GCDisplaySettings[42] в gaurabda): classic — «(Fast till noon for X, with feast tomorrow)», modern — «(Fast today for X)». По умолчанию — classic: на полной выборке из 202 городов × 2026 год против vaisnavacalendar.info под classic текст заметок совпадает с эталоном на 75.8% дат (3316/4375), под modern (дефолт самой gaurabda) — на 12.9% (561/4356). В отличие от caturmasya_system проверка на большой выборке подтвердила исходный результат — дефолт не менялся. Тот же контракт: неизвестное значение → VALIDATION_ERROR, применённое значение — в ответе рядом с caturmasya_system.

Диапазоны: год в year и параметрах-датах — 2..9998 (крайние годы исключены, поскольку расчёт использует соседние дни), month=1..12, lat -90..90, lon -180..180, timezone_offset=-14..14 часов от UTC (Алматы: 5). Нечисловые и неканонические значения не подменяются дефолтами, а возвращают VALIDATION_ERROR.

Для satya-dharma предпочтительно передавать geonames_id (один запрос к /api/locations/catalog, ничего никуда не пишется) либо координаты + TZ, если свои координаты городов уже есть на стороне сайта.

Объект location в ответе

Календарные эндпоинты (calendar-data, calendar-data/year, calendar-data/next-event, day-detail, day-detail/astronomy, day-detail/fasting-logic) возвращают в data.location резолвленную локацию — не то, что было передано в запросе, а то, что реально использовалось для расчёта:

ПолеОписание
geonames_idПри адресации по координатам — null. Отрицательный (<0) — синтетическая запись вне GeoNames (сейчас только Mayapur, см. schema.CUSTOM_CITIES): своей страницы на geonames.org у неё нет, ссылку по https://www.geonames.org/<geonames_id> строить нельзя.
coordinate_sourceИсточник фактически использованных координат: geonames, custom_catalog (курируемая точка вне GeoNames) или request (явные lat/lon из запроса).
lat, lon, altКоординаты и высота (м), по которым фактически считался календарь.
timezone_offset«Стандартный» (не-DST) числовой offset от UTC — тот же смысл, что и в параметре запроса.
tznameIANA-зона (напр. America/New_York) — при адресации по координатам null. По ней же (не по timezone_offset) в gaurabda DST-осведомлённо считаются восход/закат/титхи для городов с переходом на летнее время.
name, country, regionПри адресации по координатам — null. name — русское имя, если оно есть в каталоге, иначе исходное (та же логика, что и в /api/locations/catalog).
Координаты и часовой пояс — часть идентичности конфигурации календаря, не скрытая техническая деталь: расхождение источника точки в несколько десятков километров может поменять спорную дату праздника (см. docs/research/vaisnavacalendar-coordinate-audit.md). Собственный UI gcal показывает под выбором города строку вида «Ноксвилл, Теннесси, US» / «35.96064, -83.92074 · America/New_York · Источник: GeoNames 4634946», где GeoNames ID — ссылка на geonames.org; для курируемой точки вне GeoNames явно указывается «Источник: каталог gcal».
Публичный API всегда считает календарь по дефолту GCal/ISKCON: эфемерида gaurabda, восход — верхний край диска с номинальной рефракцией 50′, aruṇodaya — 96 минут до восхода. Исследовательские оси (альтернативные эфемериды, горизонт, aruṇodaya) доступны только с research=1 в query string и не входят в OpenAPI; см. docs/research/internal-research-api.md.

Календарные эндпоинты

GET /api/calendar-data

Сетка месяца + события.

ПараметрОписание
year, monthГод 2–9998, месяц 1–12. По умолчанию — текущие.
локациясм. выше
compact1 — только location/year/month/events[]/payload_version, caturmasya_system, fasting_note_style, event_categories (без days{}/ekadashi_events[]/caturmasya[]/gaurabda_year — нужны только собственному UI gcal, не считаются вовсе при compact). Без параметра — полный ответ, как раньше.
caturmasya_systemсм. выше. По умолчанию purnima.
fasting_note_styleсм. выше. По умолчанию classic.
event_categoriesсм. «События events[]» ниже. Список чисел 0..6 через запятую — фильтр по «важности»/типу праздника. Не передан — фильтр не применяется.

data (без compact): location, year, month, gaurabda_year, days{}, ekadashi_events[], events[], caturmasya[], payload_version, caturmasya_system, fasting_note_style, event_categories.

GET /api/calendar-data?geonames_id=1526384&year=2026&month=9
GET /api/calendar-data?geonames_id=1526384&year=2026&month=9&compact=1  # без grid-only days/ekadashi_events/caturmasya
Для satya-dharma рекомендуется compact=1 — используется только events[] (Экадаши/Махадвадаши/Чатурмасья видны как записи event_kind внутри него), в разы меньше трафика. См. «Формат ответа» про gzip — работают вместе (~11× меньше суммарно на замере: 11800 → ~1050 байт).
payload_version (сейчас v17.2) — тот же идентификатор, что gcal использует в ключе собственного кэша года (_YEAR_PAYLOAD_VERSION + EVENT_CLASSIFIER_VERSION); меняется только при изменении логики расчёта/схемы событий, не на каждый деплой. v17 отделяет кэши по точке диска, рефракции и определению aruṇodaya; суффикс .2 — версия классификатора событий.

GET /api/calendar-data/year

events[] за весь год одним запросом — без сетки (days{}) и без месячного фильтра. Отдельного расчёта не требует: год и так считается и кэшируется целиком (gcal_cache.get_year), этот эндпоинт просто не фильтрует его по месяцу.

ПараметрОписание
yearПо умолчанию — текущий.
локациясм. выше
caturmasya_systemсм. выше. По умолчанию purnima.
fasting_note_styleсм. выше. По умолчанию classic.
event_categoriesсм. «События events[]» ниже.

data: location, year, events[], payload_version, caturmasya_system, fasting_note_style, event_categories — форма ответа всегда компактная, отдельного compact-параметра нет.

GET /api/calendar-data/year?geonames_id=1526384&year=2026  # events[] за весь 2026 год, caturmasya_system=purnima + fasting_note_style=classic (дефолты)
GET /api/calendar-data/year?geonames_id=1526384&year=2026&caturmasya_system=pratipat
GET /api/calendar-data/year?geonames_id=1526384&year=2026&fasting_note_style=modern
GET /api/calendar-data/year?geonames_id=1526384&year=2026&event_categories=4  # только ISKCON's historical events среди festival

GET /api/calendar-data/next-event

Первое подходящее событие с датой date >= from. Если его нет в оставшейся части года, автоматически проверяется следующий календарный год; дальше поиск не продолжается. Использует тот же годовой кэш, но не строит и не сериализует полный events[].

ПараметрОписание
fromYYYY-MM-DD, включительно. По умолчанию — текущая дата в часовом поясе выбранной локации.
event_kindsОбязательный непустой список через запятую: ekadashi, mahadvadashi, parana, festival, caturmasya, sankranti, other.
локациясм. выше
caturmasya_systemсм. выше. По умолчанию purnima.
fasting_note_styleсм. выше. По умолчанию classic.

data: location, from, event (обычное публичное событие с добавленным date либо null), payload_version, caturmasya_system, fasting_note_style, event_categories.

GET /api/calendar-data/next-event?geonames_id=1526384&from=2026-07-31&event_kinds=ekadashi,mahadvadashi

{
  "success": true,
  "data": {
    "location": {
      "geonames_id": 1526384,
      "coordinate_source": "geonames",
      "lat": 43.25249,
      "lon": 76.9115,
      "alt": 783.0,
      "timezone_offset": 5.0,
      "name": "Алматы",
      "country": "KZ",
      "region": "Алматы",
      "tzname": "Asia/Almaty"
    },
    "from": "2026-07-31",
    "event": {
      "date": "2026-08-09",
      "event_code": "EKADASHI",
      "event_kind": "ekadashi",
      "text": "Fasting for Kamika Ekadasi"
    },
    "payload_version": "v17.2",
    "caturmasya_system": "purnima",
    "fasting_note_style": "classic",
  }
}
Версия HTTP-контракта — 1.2.2 (info.version в OpenAPI). payload_version версионирует содержимое годового расчётного payload и ключ его кэша, а не набор доступных маршрутов; текущее значение — v17.2.

GET /api/day-detail

Детали одного дня (времена, сегменты титхи, накшатра, йога, раши Солнца и Луны, экадаши/парана, sankranti).

ПараметрОписание
dateОбязателен, YYYY-MM-DD
локациясм. выше
caturmasya_systemсм. выше. По умолчанию purnima.
fasting_note_styleсм. выше. По умолчанию classic.
event_categoriesсм. «События events[]» ниже.

Расширенный панчанга: nakshatra_elapsed_percent (0..<100), nakshatra_pada (1..4), yoga/yoga_name (0..26), sun_rasi/sun_rasi_name и moon_rasi/moon_rasi_name (0..11). Значения относятся к восходу дня. Эти поля не добавляются в compact-ответы и /api/calendar-data/year.

date_timezone_offset — фактическое дневное UTC-смещение на выбранную дату с учётом DST (режим в местный полдень, используемый суточным расчётом Gaurabda). Это не замена IANA-зоне для каждого момента: в ночь перехода смещение в 00:00 может отличаться, поэтому готовые JD форматируются по tzname. В ekadashi{} явно отдаются ekadashi_date и fast_day; у постового дня parana{} содержит date, time1, time2. Поэтому клиенту не нужно выводить относительное «завтра» или самостоятельно вычислять дату перенесённого поста/паранама.

Структурные признаки: is_ksaya, ksaya_tithi (1..30 или null), vriddhi_day (1, 2 или null), masa_start/masa_end. Они формируются из соседних дней, уже рассчитанных GCal, без включения английских display-заметок. is_ksaya стоит на дне после пропущенной титхи; vriddhi_day различает первый и второй восход повторённой титхи. Те же поля есть у дня в полном days{}; в compact и годовой endpoint не входят.

moonrise_time/moonset_time — местное HH:MM либо null, если события нет внутри этих гражданских суток. Расчёт точечный, топоцентрический, для центра диска Луны на горизонте 0° без рефракции; дорогие годовые ключи GCDisplaySettings[4]/[5] не включаются. Нативное ядро включается только после self-check с Python-путём, а результат дня лениво кэшируется в ограниченной памяти worker'а. На независимой сверке 1825 городо-дней со Swiss Ephemeris совпадение наличия событий — 100%, медиана времени около 2.8 сек, максимум 18.7 сек.

arunodaya_offset_minutes показывает фактический интервал до восхода; night_muhurta_minutes — одну пятнадцатую предыдущей фактической ночи. При fixed-варианте первое поле равно 96 минутам, а второе остаётся сезонным справочным значением; при temporal-варианте первое равно удвоенному второму.

GET /api/day-detail?geonames_id=1526384&date=2026-09-04

GET /api/day-detail/astronomy

Ленивая астрономическая часть объединённой модалки дня. Принимает обязательные date=YYYY-MM-DD и локацию. Расширенный набор (карана, pāda, мухурты, транзиты) — только с research=1&extended_panchanga=1; см. docs/research/internal-research-api.md. Имеет отдельный SQLite/LRU-кэш и не меняет годовой payload_version.

Соглашения расчёта: титхи/карана используют геоцентрическую разность долгот центров Луны и Солнца; накшатра, pāda и rāśi Луны — геоцентрическую долготу центра Луны с GCal Lahiri ayanāṃśa; нитья-йога — сумму долгот центров с двойной ayanāṃśa. Край диска, горизонт и рефракция для этих величин не участвуют. В default-режиме центр Солнца находится на −0.833° потому, что выбран верхний край (+16′) и фиксированная рефракция (+34′); альтернативы задаются независимыми ключами выше. Moonrise/moonset — топоцентрический центр Луны на геометрическом горизонте 0° без рефракции. Высоты на графике также геометрические, без рефракции.

На UI участки кривых выше горизонта сплошные, ниже — пунктирные. События Солнца подписаны сверху, события Луны/титхи/смены раши — снизу.

GET /api/day-detail/astronomy?geonames_id=1526384&date=2026-09-04

GET /api/day-detail/fasting-logic

Пояснение выбора дня поста экадаши для QA-UI (кнопка «Почему этот день?» в модалке дня). Читает уже кэшированный год, астрономию не пересчитывает, payload_version не меняет. 404, если дата не из окна календарной экадаши / дня поста / паранам.

В steps[] только сработавшие проверки GCal (MahadvadasiCalcEkadasiCalc → окно паранам), без перечня отпавших соседних типов. sources[] — цитаты Hari-bhakti-vilāsa по сработавшему типу.

ПараметрОписание
dateОбязателен, YYYY-MM-DD.
локациясм. выше.
caturmasya_systemсм. выше. По умолчанию purnima.
fasting_note_styleсм. выше. По умолчанию classic.
GET /api/day-detail/fasting-logic?geonames_id=1526384&date=2026-08-24

GET /api/panchanga-timeline (internal)

Внутренний/отладочный эндпоинт (x-internal в OpenAPI). QA UI и satya-dharma его не вызывают; та же логика используется внутри day-detail/astronomy. Точные по модели GCal моменты начала титхи, санкранти, накшатры и йоги в полуоткрытом интервале местных гражданских дат [start, end). Расчёт ленивый и не изменяет годовой/compact payload.

ПараметрОписание
startОбязателен, YYYY-MM-DD, включительно.
endОбязателен, YYYY-MM-DD, не включается; интервал от 1 до 32 дней.
локациясм. выше; нужна для локального времени и границ гражданских дат.
typestithi,sankranti (по умолчанию); также допустимы nakshatra, yoga, conjunction, rahu_kalam, yama_ghanti, guli_kalam, abhijit.
researchТолько для QA/research: 1 включает исследовательские оси расчёта и metadata в ответе; без ключа — GCal default, override молча игнорируется. См. docs/research/internal-research-api.md.

data: location, start, end, types, events[]. С research=1 дополнительно metadata расчёта (см. internal-research-api). Общие поля события: event_kind, at_utc, local_date, local_time. У tithi_start: tithi_number (1..30), tithi_name; у sankranti и conjunction: rasi (0..11), rasi_name; у nakshatra_start: nakshatra/nakshatra_name (0..26); у yoga_start: yoga/yoga_name (0..26).

Это GCal-derived boundary times, а не Swiss-Ephemeris timestamps. Независимая проверка за 2023–2027 показала максимальное расхождение 3.1 минуты для титхи, 18.9 минуты для санкранти, 1.5 минуты для накшатры, 1.8 минуты для йоги и 2.5 минуты для соединения; раши соединения совпали в 248/248 проверок. Endpoint не добавляет поля в compact и не требует изменения payload_version.
GET /api/panchanga-timeline?geonames_id=1526384&start=2026-04-01&end=2026-05-01
GET /api/panchanga-timeline?geonames_id=1526384&start=2026-04-01&end=2026-05-01&types=sankranti
GET /api/panchanga-timeline?geonames_id=1526384&start=2026-04-01&end=2026-05-01&types=nakshatra,yoga
GET /api/panchanga-timeline?geonames_id=1526384&start=2026-04-01&end=2026-05-01&types=conjunction

GET /api/research/arunodaya-scales (internal)

Внутренний/исследовательский эндпоинт (x-internal в OpenAPI). Только с research=1 (иначе 404). QA-графики трёх шкал aruṇodaya за год; satya-dharma не вызывает. Не меняет year-payload. Подробности — docs/research/internal-research-api.md.

ПараметрОписание
yearГод; без ключа — текущий гражданский год.
локациясм. выше.
researchОбязателен: 1, true или yes.

data: location, year, days[] с часами от местной полуночи (sunrise_hour, arunodaya_96_hour, arunodaya_215_hour, arunodaya_1212_hour, solar6_hour; null в полярные сутки). Плюс metadata расчёта. Выбор arunodaya_definition на состав рядов не влияет — всегда все три шкалы.

GET /api/research/arunodaya-scales?geonames_id=1526384&year=2026&research=1

События events[]

Каждый элемент: { date?, event_code, event_kind, text, event_category?, event_category_name?, fasttype?, fastsubject? }.

event_categories=0,4 (все три эндпоинта календаря) — список чисел 0..6 через запятую, оставляет в events[] только festival-события этих категорий. Остальные event_kind (экадаши, парана, Чатурмасья, санкранти...) фильтр не трогает — у них категории нет, они приходят всегда. Параметр не передан — фильтр не применяется. Неизвестное число → VALIDATION_ERROR. Применённый фильтр отражён в ответе как event_categories (null, если не передавали).

is_fast — не поле events[] (элементы события его не несут, это раньше было ошибочно указано здесь). Это отдельное булево поле на уровне дня — внутри days{} (полный, не-compact ответ calendar-data) и на верхнем уровне ответа /api/day-detail. Означает только пост экадаши/махадвадаши (есть запись в ekadashi_events на этот день), не пост при явлении святого — тот определяется по fasttype/fastsubject самого festival-события в events[]. satya-dharma, работающий только с compact=1/events[], это поле не видит и не использует.

Справочник локаций

МетодПутьНазначение
GET/api/locationsДинамический список городов UI gcal — те, для кого уже посчитан кэш года, не статичный справочник: limit? 1–200, sort? recent (по умолчанию, недавность обращения) или frequent (суммарный access_count); каждая запись отдаёт use_count. Ответ — private, no-cache; UI запрашивает его заново при загрузке/F5, но не при переходе стрелками между месяцами.
GET/api/locations/catalogПоиск в каталоге GeoNames: search?, limit? 1–200, compact? 1 — только geonames_id/name/name_ru/country/region/population/timezone_offset
GET/api/locations/nearestБлижайшие из city_catalog (весь GeoNames, ~170k): обязательные lat=-90..90, lon=-180..180, limit? 1–20. Отдаёт geonames_id, не id — использовать с /api/calendar-data?geonames_id=...

GeoNames — подход

Единственный справочник локацийcity_catalog (~170k городов, сид из data/cities_geonames.csv при первом старте БД, плюс несколько записей вне GeoNames — Kosshy/Mayapur, см. schema.CUSTOM_CITIES). Отдельного куррированного справочника с внутренним id больше нет — всё адресуется по geonames_id.

Поиск Mayapur возвращает две географически разные точки. Собственный UI ставит первой «Маяпур — ISKCON, рекомендуемая точка» (geonames_id=-1, 23.423413, 88.388079) и отдельно подписывает GeoNames 1263253 как «другая деревня» — она находится примерно в 116 км и для календаря ISKCON-Маяпуры не подходит.

Типичный поток для UI / сайта:

  1. GET /api/locations/catalog?search=Алматы — найти город (поля: id каталога — служебный, не для адресации, name, name_ru, lat, lon, timezone_offset, country, geonames_id, region, population, tzname — IANA-зона, напр. Asia/Almaty; результаты отсортированы по населению, крупные города — в начале списка); timezone_offset в этом ответе — «стандартный» (не-DST) числовой offset, как и раньше — но сам расчёт календаря (events[]/days{} из /api/calendar-data) DST-осведомлён: для городов с переходом на летнее время восход/закат/титхи/накшатра/санкранти считаются по фактическому смещению на каждую дату (по tzname из каталога), не по одному фиксированному числу на весь год;
  2. дальше считать календарь по geonames_id из найденной записи — ничего никуда не пишется: GET /api/calendar-data?geonames_id=<geonames_id>&...;
  3. либо, если своих координат у сайта уже достаточно, — сразу lat/lon/timezone_offset без обращения к каталогу вовсе.
CSV GeoNames в образе — только для сида. Интегратору не нужно читать файл: работайте через /api/locations/catalog.
Для satya-dharma (адресация только по geonames_id, остальное — для city picker'а) рекомендуется compact=1: geonames_id/name/name_ru/country/region/population/timezone_offset — в разы меньше трафика, чем полный ответ, но всё ещё различает одноимённые города (в каталоге реально встречаются разные города с одинаковым name_ru в одной стране — напр. три разных "Aktau"/"Актау" в Казахстане; без region/population в подсказках city picker'а они были бы неразличимы). Собственный поиск города в UI gcal тоже ходит с compact=1timezone_offset в компакте хватает для подписи в <select>.
# поиск (компактно, для автокомплита)
curl -sS 'BASE/api/locations/catalog?search=Almaty&limit=5&compact=1'

# сразу расчёт по geonames_id из результата поиска, компактный ответ
curl -sS 'BASE/api/calendar-data?geonames_id=1526384&year=2026&month=9&compact=1'

# ближайшая Экадаши/Махадвадаши для hero
curl -sS 'BASE/api/calendar-data/next-event?geonames_id=1526384&from=2026-07-31&event_kinds=ekadashi,mahadvadashi'

Служебное

APP_VERSION → 2.5.12 (SemVer, показан в шапке QA-календаря)
GET /health → { "status": "ok", "version": "..." }  # build GIT_SHA
GET /openapi.json → OpenAPI 3.1, JSON
GET /       → HTML QA-календарь
GET /docs   → эта страница