# Справочник API rustata.ru

Открывать, когда нужно точное имя параметра, поля ответа или код ошибки. Правила и ловушки живут
в `SKILL.md`: их надо знать ДО запроса, а не искать после.

## GET /api/v1/data - срез по территориям

| Параметр | Смысл |
|---|---|
| `dataset` | `fedstat/indicator_31448` или просто код набора. Обязателен |
| `geo` | **уровень** `country`/`district`/`region`/`city`/`all` **или** коды через запятую (ОКАТО или canonical `RU-46`) |
| `class` | код продукта, несколько через запятую, `all` - все продукты набора |
| `measure`, `transformation`, `freq`, `unit` | остальные измерения ряда |
| `start`, `end` | период, можно месяцем: `2024-01` |
| `format` | `json` (по умолчанию) или `csv` с разделителем `;` |
| `labels` | `1` - добавить `class_label` рядом с кодом продукта |
| `bom` | `1` - BOM в CSV, чтобы Excel на Windows открыл двойным кликом |
| `limit`, `offset` | страница **по рядам**: 100 по умолчанию, максимум 300 |

Ответ JSON длинный (tidy): отдельно `series` с территорией и измерениями, отдельно `observations`,
соединяются по `series_key`. Постранично идут РЯДЫ, а не значения: ряд приходит целиком по
времени, склеивать историю не нужно. В ответе `series_total`, `has_more`, `next_offset`.

Поля ряда: `series_key`, `series_ref`, `title` (витринное), `title_raw` (складское), коды измерений,
`geo`/`geo_canonical`/`geo_label`/`geo_level`, `last_period`, `status`, `provider_code`,
`dataset_code`. Поле `dataset` в конверте несёт статус набора: `status`, `latest_period`, `freq`,
`sla_months`, `lag_months`, `overdue_by`.

Поля значения: `series_key`, `period` (первый день периода), `value`, `as_of`. Значения
канонические: на период отдаётся последняя по времени публикации оценка, `as_of` показывает какая.

`all` у измерений ряда (`class`, `measure`, `transformation`, `freq`, `unit`) значит то же, что
пустой параметр, но пишем его явно: пустое значение читается как забытое. У `dataset` такого
escape нет намеренно - там `all` скорее опечатка, и молча отдать весь склад хуже ошибки.

Признак «перестало публиковаться» считается двумя мерками. Набор - от сегодня по SLA частоты
(W 1 мес, M 3, Q 6, A 20). Ряд - от фронтира набора той же частоты (W/D 3 мес, M 4, Q 9, A 18);
это те же пороги, по которым витрина ставит бейдж «не обновляется», поэтому сайт и API отвечают
одинаково. Мерить ряд от «сегодня» нельзя: набор сам выходит с задержкой, и снятыми выглядели бы
все ряды сразу после публикации.

CSV, порядок колонок фиксирован:
`series_key;class;measure;transformation;freq;unit;geo;geo_label;geo_level;period;value;as_of;last_period;status`.
Колонки измерений выводятся только те, что у набора есть; `class_label` встаёт сразу за `class`
при `labels=1`.

Потолок ответа - 60 000 значений (~6 МБ), дальше `400 SLICE_TOO_BIG`. Кэш: `ETag` и
`Cache-Control: max-age=3600`, повтор с `If-None-Match` даёт `304` за ~10 мс и квоту не тратит.
ETag считается от параметров и последней даты публикации в наборе, поэтому пересмотр старого
значения кэш сбрасывает.

## Остальные маршруты

| Маршрут | Зачем |
|---|---|
| `/api/v1/reference/datasets`, `/geo?level=`, `/codes?dataset=` | справочники; `format=csv`, `bom=1`. У наборов `status`/`sla_months`/`overdue_by`, у кодов `stalled`/`last_period` |
| `/api/v1/series/search` | найти ряд; отдаёт `series_key`. Параметры: `q`, `limit`, `provider`, `dataset`, `geo`, `freq`, `class`, `transformation` |
| `/api/v1/series/{series_key}/observations?as_of=` | один ряд; `as_of=latest` или дата, `all` не поддержан |
| `/api/v1/series/{series_key}/evidence` | происхождение: релизы, файл источника, отпечаток, парсер |
| `/api/v1/concepts/search`, `/concepts/{id}` | тематическое дерево: концепт, а не ряд |
| `/api/v1/odata/observations`, `/odata/series` | канал для Excel |
| `/api/openapi.json`, `/api/docs` | машиночитаемая схема и Swagger; только `/api/v1/*` |
| `/health` | без токена, для проверки прода |

## Лимиты

Квота считается **в значениях, а не в запросах**. Бесплатный тариф - 1 000 000 значений в месяц и
20 запросов в минуту; академический и коммерческий - без месячной квоты, 60 и 120 запросов в
минуту. Живая таблица - `/api/limits`, она строится из `api.services.accounts.PLANS`. Справочники
квоту в значениях не расходуют, минутный лимит общий. Отклонённый запрос квоту не тратит.

Заголовки: `X-Request-Id`, `X-Quota-Values-Limit`/`-Remaining`,
`X-RateLimit-Limit-Minute`/`-Remaining-Minute`, при 429 - `Retry-After`.

- Заголовки квоты значений приходят только у тарифов с месячной квотой. У `academic`, `business`
  и `owner` их нет вовсе, и это не сбой.
- Регистр имён не совпадает с документацией: прод отдаёт `X-Ratelimit-Remaining-Minute`, контракт
  обещает `X-RateLimit-…`. Ищем в нижнем регистре; в клиенте это делает `rustata.limits()`.

## Ошибки

Конверт одинаковый на всех путях `/api/*` (страницы сайта отдают HTML):

```json
{ "error": { "code": "RATE_LIMITED", "message": "…" }, "request_id": "req_4325b22dc77347b1" }
```

| Код | Что делать |
|---|---|
| `UNAUTHENTICATED` | заголовка нет вовсе: проверь, что `RUSTATA_TOKEN` доехал до процесса |
| `INVALID_TOKEN` | токен неизвестен или отозван - смотри список в кабинете |
| `RATE_LIMITED` | 429: ждём `Retry-After`, не долбим. Месячная квота - до начала месяца |
| `SLICE_TOO_BIG` | сузить период или `limit`, а не повторять |
| `NOT_FOUND` | у `/data` значит «под фильтры рядов не нашлось»: первым делом проверь код продукта |
| `DATASET_REQUIRED` | у `/reference/codes` забыт `dataset` |

`X-Request-Id` сохраняем в логах: спорные случаи разбираются по нему, а токены из журнала
обращений вычищаются (`api_gateway.mask_tokens_in_logs`).

## Где это живёт в коде

Шлюз доступа - `services/web-service/src/web/api_gateway.py` (токен, квоты, конверт ошибок),
тарифы и счётчики - `api/services/accounts.py` (SQLite на томе, не Postgres: ADR-0020), маршруты -
`services/api-service/src/api/routers/`, разбор фильтров - `bulk_service._dim_filters`, свежесть -
`api/services/freshness_service.py`. Маршруты api-service смонтированы в веб-процесс, отдельного
API-контейнера в проде нет.

Контракт целиком - `docs/API_CONTRACT.md`, доступ - `docs/adr/0020-accounts-and-api-access.md`,
страница для людей - https://rustata.ru/api/methods.
