методы API
Срез по территориям, поиск рядов и показателей, значения одного ряда, происхождение цифр и справочники - чтобы знать, что подставлять в параметры. У каждого метода - готовый пример на Python, R и Julia.
Во всех примерах токен передаётся заголовком Authorization: Bearer;
подходит и X-API-Key. Тарифы и квоты - на отдельной странице.
GET /api/v1/data - срез по территориям
Главный метод. Отдаёт показатель сразу по многим территориям, поэтому «цены на пиво по всем регионам» - это один запрос, а не восемьдесят пять. Так же выгоднее по квоте: она считается в значениях, и один срез стоит ровно столько, сколько в нём точек.
Параметры
| Параметр | Что задаёт |
|---|---|
dataset | набор данных, например fedstat/indicator_31448. Обязателен |
geo | уровень территории или перечень кодов через запятую |
class | код продукта, несколько через запятую, либо all - все продукты набора |
measure, transformation | вид показателя и преобразование |
freq, unit | частота и единица измерения |
start, end | период; можно писать месяцем: 2024-01 |
format | json (по умолчанию) или csv |
limit, offset | сколько рядов взять и с какого начать |
bom | только для CSV: 1 добавляет BOM для Excel на Windows |
Уровни территории
geo | Что вернётся | Территорий |
|---|---|---|
country | Россия целиком | 1 |
district | федеральные округа | 13 с историческими |
macroregion | экономические районы (прежнее деление) | 11 |
region | субъекты федерации | до 85 |
city | города и районы внутри субъектов | сотни |
all | все уровни сразу | - |
46000000000,RU-50 | только перечисленные территории | по списку |
Коды принимаются в обоих видах: исходный ОКАТО и нормализованный
(RU-46). Полный перечень - в справочнике территорий.
Продукты
В наборе обычно не один продукт, а сотни: в ценах на продовольствие их 875.
Параметр class работает так же, как geo - можно один код, можно список,
можно всё сразу.
class | Что вернётся |
|---|---|
3203 | один продукт |
3203,7802 | только перечисленные, порядок не важен |
all | все продукты набора |
| не передан | то же, что all |
Отсюда берётся, например, «все цены по Москве одним запросом»:
geo=45000000000&class=all. Ряды при этом отдаются страницами по
limit: сколько их всего, видно в поле series_total ответа.
Коды и их названия - в справочнике на сайте и методом
/api/v1/reference/codes в программе.
Россия приходит двумя рядами
«Российская Федерация» и «Российская Федерация без учёта новых субъектов (с 01.01.2023)» -
оба на уровне country.
Это не дубль, а две методики счёта. Различить их можно по полю geo_label,
выбор между ними за вами.
Что приходит в ответе
Ответ состоит из двух частей: список рядов с описанием территории и плоский список значений, по строке на каждое. Такая форма разворачивается в таблицу «период на территории» одной строкой кода и не требует разбирать многоэтажную шапку исходных выгрузок.
{
"series": [
{ "series_key": "s1_23sqev7ozzer4t73", "geo": "14000000000",
"geo_label": "Белгородская область", "geo_level": "region",
"title": "Пиво отечественное, л", "freq": "M", "unit": "RUB" }
],
"observations": [
{ "series_key": "s1_23sqev7ozzer4t73", "period": "2025-06-01",
"value": 198.23, "as_of": "2025-06-01" }
],
"series_count": 100, "series_total": 470,
"offset": 0, "has_more": true, "next_offset": 100
}
На каждый период приходит одно значение - последняя опубликованная оценка.
Рядом идёт as_of: дата публикации именно этой оценки. Статистику уточняют задним
числом, и по этому полю видно, какая версия цифры у вас на руках.
В CSV те же данные с разделителем ; и колонками
series_key;geo;geo_label;geo_level;period;value;as_of. Кодировка UTF-8 без BOM: так
её ждут pandas, readr и CSV.jl. Для Excel на Windows добавьте &bom=1, иначе
кириллица откроется кракозябрами.
Средняя цена пива по регионам с 2024 года, развёрнутая в таблицу «период на территории»
import pandas as pd
import requests
TOKEN = "rst_ваш_токен"
resp = requests.get(
"https://rustata.ru/api/v1/data",
params={"dataset": "fedstat/indicator_31448", "class": "3203",
"geo": "region", "start": "2024-01"},
headers={"Authorization": f"Bearer {TOKEN}"},
timeout=60,
)
resp.raise_for_status()
payload = resp.json()
series = pd.DataFrame(payload["series"]).set_index("series_key")
obs = pd.DataFrame(payload["observations"])
obs["region"] = obs["series_key"].map(series["geo_label"])
obs["period"] = pd.to_datetime(obs["period"])
table = obs.pivot(index="period", columns="region", values="value")
print(table.tail())
# у тарифов без месячной квоты этого заголовка нет, поэтому .get, а не []
print("остаток квоты:", resp.headers.get("X-Quota-Values-Remaining", "без ограничения"))
library(httr)
library(readr)
library(tidyr)
# Windows: если консоль печатает кириллицу как <U+0411><U+0435>…, дело в локали сессии,
# а не в данных - сами строки целы. Лечится одной строкой:
if (.Platform$OS.type == "windows") Sys.setlocale("LC_CTYPE", "Russian_Russia.utf8")
token <- "rst_ваш_токен"
resp <- GET(
"https://rustata.ru/api/v1/data",
query = list(dataset = "fedstat/indicator_31448", class = "3203",
geo = "region", start = "2024-01", format = "csv"),
add_headers(Authorization = paste("Bearer", token))
)
stop_for_status(resp)
# content(resp, "raw") + locale(): readr сам разбирает UTF-8, без промежуточной строки
df <- read_delim(content(resp, "raw"), delim = ";",
locale = locale(encoding = "UTF-8"), show_col_types = FALSE)
table <- pivot_wider(df, id_cols = period, names_from = geo_label, values_from = value)
head(table)
using HTTP, CSV, DataFrames
token = "rst_ваш_токен"
resp = HTTP.get("https://rustata.ru/api/v1/data";
query = Dict("dataset" => "fedstat/indicator_31448", "class" => "3203",
"geo" => "region", "start" => "2024-01", "format" => "csv"),
headers = ["Authorization" => "Bearer $token"])
df = CSV.read(IOBuffer(resp.body), DataFrame; delim = ';')
table = unstack(df, :period, :geo_label, :value)
first(table, 5)
GET /api/v1/series/search - поиск рядов
Ищет ряды по строке и возвращает их series_key - с ним потом
берутся значения и происхождение. Нужен, когда код продукта заранее неизвестен.
Найти ряд по названию и получить его ключ
import requests
TOKEN = "rst_ваш_токен"
resp = requests.get(
"https://rustata.ru/api/v1/series/search",
params={"q": "бензин аи-92", "limit": 5},
headers={"Authorization": f"Bearer {TOKEN}"}, timeout=30,
)
resp.raise_for_status()
for item in resp.json()["items"]:
print(item["series_key"], "|", item["title"])
library(httr)
library(jsonlite)
token <- "rst_ваш_токен"
resp <- GET("https://rustata.ru/api/v1/series/search",
query = list(q = "бензин аи-92", limit = 5),
add_headers(Authorization = paste("Bearer", token)))
stop_for_status(resp)
items <- fromJSON(content(resp, "text", encoding = "UTF-8"))$items
items[, c("series_key", "title")]
using HTTP, JSON3
token = "rst_ваш_токен"
resp = HTTP.get("https://rustata.ru/api/v1/series/search";
query = Dict("q" => "бензин аи-92", "limit" => 5),
headers = ["Authorization" => "Bearer $token"])
for item in JSON3.read(resp.body).items
println(item.series_key, " | ", item.title)
end
GET /api/v1/series/{series_key}/observations
Все значения одного ряда. Нужен, когда ключ уже известен: получен поиском или взят из адреса страницы ряда.
Для разового скачивания метод не нужен - на странице ряда есть кнопка «скачать CSV», она работает без токена.
История одного ряда с начала 2024 года
import pandas as pd
import requests
TOKEN = "rst_ваш_токен"
KEY = "s1_mydzoqiytm4xqjq3"
resp = requests.get(
f"https://rustata.ru/api/v1/series/{KEY}/observations",
params={"start": "2024-01"},
headers={"Authorization": f"Bearer {TOKEN}"}, timeout=30,
)
resp.raise_for_status()
df = pd.DataFrame(resp.json()["observations"])
df["period"] = pd.to_datetime(df["period"])
print(df.set_index("period")["value"].tail())
library(httr)
library(jsonlite)
token <- "rst_ваш_токен"
key <- "s1_mydzoqiytm4xqjq3"
resp <- GET(paste0("https://rustata.ru/api/v1/series/", key, "/observations"),
query = list(start = "2024-01"),
add_headers(Authorization = paste("Bearer", token)))
stop_for_status(resp)
obs <- fromJSON(content(resp, "text", encoding = "UTF-8"))$observations
tail(obs[, c("period", "value")])
using HTTP, JSON3, DataFrames
token = "rst_ваш_токен"
key = "s1_mydzoqiytm4xqjq3"
resp = HTTP.get("https://rustata.ru/api/v1/series/$key/observations";
query = Dict("start" => "2024-01"),
headers = ["Authorization" => "Bearer $token"])
obs = DataFrame(JSON3.read(resp.body).observations)
last(obs, 5)
GET /api/v1/series/{series_key}/evidence
Происхождение значений: из какого файла источника они пришли, каким релизом опубликованы, каким прогоном загружены. Этим ответ на вопрос «откуда цифра» отличается от «скачал где-то в интернете».
Откуда взялись значения ряда
import requests
TOKEN = "rst_ваш_токен"
KEY = "s1_mydzoqiytm4xqjq3"
resp = requests.get(
f"https://rustata.ru/api/v1/series/{KEY}/evidence",
headers={"Authorization": f"Bearer {TOKEN}"}, timeout=30,
)
resp.raise_for_status()
for release in resp.json()["releases"][:3]:
print(release["published_at"], "|", release["source_file"])
library(httr)
library(jsonlite)
token <- "rst_ваш_токен"
key <- "s1_mydzoqiytm4xqjq3"
resp <- GET(paste0("https://rustata.ru/api/v1/series/", key, "/evidence"),
add_headers(Authorization = paste("Bearer", token)))
stop_for_status(resp)
releases <- fromJSON(content(resp, "text", encoding = "UTF-8"))$releases
head(releases[, c("published_at", "source_file")], 3)
using HTTP, JSON3
token = "rst_ваш_токен"
key = "s1_mydzoqiytm4xqjq3"
resp = HTTP.get("https://rustata.ru/api/v1/series/$key/evidence";
headers = ["Authorization" => "Bearer $token"])
for release in JSON3.read(resp.body).releases[1:min(3, end)]
println(release.published_at, " | ", release.source_file)
end
Показатели: /api/v1/concepts/search и /api/v1/concepts/{id}
Поиск по показателям, а не по отдельным рядам, и карточка показателя со сводкой: сколько рядов, по каким территориям, какие преобразования. Удобно, когда нужно понять, что вообще есть по теме, прежде чем забирать данные.
Найти показатель и посмотреть его сводку
import requests
TOKEN = "rst_ваш_токен"
headers = {"Authorization": f"Bearer {TOKEN}"}
found = requests.get("https://rustata.ru/api/v1/concepts/search",
params={"q": "молоко", "limit": 3},
headers=headers, timeout=30).json()
for concept in found["items"]:
print(concept["concept_id"], "|", concept["label"], "|", concept["series_count"], "рядов")
first_id = found["items"][0]["concept_id"]
card = requests.get(f"https://rustata.ru/api/v1/concepts/{first_id}",
headers=headers, timeout=30).json()
print(card["label"], "- территорий:", card.get("geo_count"))
library(httr)
library(jsonlite)
token <- "rst_ваш_токен"
auth <- add_headers(Authorization = paste("Bearer", token))
found <- fromJSON(content(GET("https://rustata.ru/api/v1/concepts/search",
query = list(q = "молоко", limit = 3), auth),
"text", encoding = "UTF-8"))
found$items[, c("concept_id", "label", "series_count")]
card <- fromJSON(content(GET(paste0("https://rustata.ru/api/v1/concepts/",
found$items$concept_id[1]), auth),
"text", encoding = "UTF-8"))
card$label
using HTTP, JSON3
token = "rst_ваш_токен"
auth = ["Authorization" => "Bearer $token"]
found = JSON3.read(HTTP.get("https://rustata.ru/api/v1/concepts/search";
query = Dict("q" => "молоко", "limit" => 3), headers = auth).body)
for concept in found.items
println(concept.concept_id, " | ", concept.label, " | ", concept.series_count)
end
card = JSON3.read(HTTP.get("https://rustata.ru/api/v1/concepts/$(found.items[1].concept_id)";
headers = auth).body)
println(card.label)
GET /api/v1/reference/* - справочники
Три списка, из которых берутся значения параметров: наборы для
dataset, территории для geo, коды продуктов для class. На
сайте они есть страницами, но программе нужен ответ, а не страница - иначе коды переписывают
с экрана руками.
| Метод | Что отдаёт |
|---|---|
/api/v1/reference/datasets | наборы: код, название, частота, сколько рядов и территорий, за какой период есть данные |
/api/v1/reference/geo | территории: код, название, уровень. Параметр level сужает до одного уровня |
/api/v1/reference/codes | коды продуктов одного набора. Параметр dataset обязателен |
Все три принимают format=csv и bom=1 - справочник
чаще всего нужен таблицей, чтобы положить рядом со своими данными и сматчить. Квоту в значениях
они не расходуют: это метаданные, а не наблюдения.
Общего справочника кодов нет намеренно
У каждого набора свой классификатор: код 3203 в ценах и в производстве означает
разные вещи.
Объединённый список подталкивал бы подставить код не в тот набор - и запрос вернул бы данные, только не те, что ожидались.
Найти набор, взять его коды и выгрузить нужные продукты
import pandas as pd
import requests
TOKEN = "rst_ваш_токен"
headers = {"Authorization": f"Bearer {TOKEN}"}
BASE = "https://rustata.ru/api/v1"
codes = pd.DataFrame(requests.get(f"{BASE}/reference/codes",
params={"dataset": "fedstat/indicator_31448"},
headers=headers, timeout=60).json()["items"])
beer = codes[codes["name"].str.contains("Пиво", case=False)]
print(beer[["class", "name"]])
# коды списком - один запрос вместо запроса на каждый продукт
data = requests.get(f"{BASE}/data",
params={"dataset": "fedstat/indicator_31448",
"class": ",".join(beer["class"]),
"geo": "region", "start": "2024-01"},
headers=headers, timeout=120).json()
print(data["series_total"], "рядов")
library(httr)
library(jsonlite)
token <- "rst_ваш_токен"
auth <- add_headers(Authorization = paste("Bearer", token))
base <- "https://rustata.ru/api/v1"
codes <- fromJSON(content(GET(paste0(base, "/reference/codes"),
query = list(dataset = "fedstat/indicator_31448"), auth),
"text", encoding = "UTF-8"))$items
beer <- codes[grepl("Пиво", codes$name, ignore.case = TRUE), ]
beer[, c("class", "name")]
data <- fromJSON(content(GET(paste0(base, "/data"),
query = list(dataset = "fedstat/indicator_31448",
class = paste(beer$class, collapse = ","),
geo = "region", start = "2024-01"), auth),
"text", encoding = "UTF-8"))
data$series_total
using HTTP, JSON3, DataFrames
token = "rst_ваш_токен"
auth = ["Authorization" => "Bearer $token"]
base = "https://rustata.ru/api/v1"
codes = DataFrame(JSON3.read(HTTP.get("$base/reference/codes";
query = Dict("dataset" => "fedstat/indicator_31448"), headers = auth).body).items)
beer = filter(row -> occursin("Пиво", row.name), codes)
data = JSON3.read(HTTP.get("$base/data";
query = Dict("dataset" => "fedstat/indicator_31448",
"class" => join(beer.class, ","),
"geo" => "region", "start" => "2024-01"), headers = auth).body)
println(data.series_total, " рядов")
Правила, общие для всех методов
Постраничная выдача
За один раз приходит 100 рядов, параметром limit
можно взять до 300. Сколько подходит всего, видно в series_total, а
next_offset подсказывает, с чего начать следующую страницу.
Постранично идут ряды, а не значения: каждый ряд приходит целиком по времени, склеивать разрезанную историю не нужно.
Если срез выбирает больше 60 000 значений, ответ не придёт -
вернётся SLICE_TOO_BIG с подсказкой сузить запрос.
Кэширование
Ответы отдаются с ETag. Передайте его обратно в
If-None-Match - если данные не менялись, придёт 304 без тела, и квота
не израсходуется. На регулярных обновлениях это экономит и время, и лимит.
Ошибки
Ошибка приходит одинаково устроенным объектом: код, понятное сообщение и подсказка, что делать.
{ "error": { "code": "SLICE_TOO_BIG",
"message": "Запрос выбирает 1 200 000 значений, максимум 200 000",
"hint": "Сузьте период или список территорий" } }
| Код | HTTP | Когда |
|---|---|---|
UNAUTHORIZED | 401 | токен не передан или отозван |
RATE_LIMITED | 429 | исчерпана квота тарифа |
SLICE_TOO_BIG | 400 | запрос выбирает слишком много данных |
NOT_FOUND | 404 | под фильтры ничего не подошло |
INTERNAL | 500 | ошибка на нашей стороне |
Интерактивный справочник
Запрос можно выполнить своим токеном прямо в браузере - Swagger. Машиночитаемое описание - openapi.json.