База тендеров и госзакупок: как собрать её у себя
Выгрузка закупок в свою базу делается курсорной пагинацией: POST /tenders/query с фильтром, затем повторные запросы с полученным курсором — до 100 объектов на страницу, без лимита на объём в сутки. Главное решение принимается до первого запроса: нужен разовый дамп или база, которую придётся держать актуальной. От этого зависит и архитектура, и стоимость.
Разовый дамп или актуальная база
За «базой тендеров» приходят с двумя разными задачами, и путать их дорого.
| Разовый дамп | Актуальная база | |
|---|---|---|
| Зачем | Аналитика за период, обучение модели, оценка рынка | Продукт, мониторинг, CRM, скоринг |
| Как читать | Курсор до конца выборки один раз | Курсор сохраняется и продолжается по расписанию |
| Расход | Разовый, предсказуемый | Небольшой постоянный: платятся только изменения |
| Частая ошибка | Выгрузить «всё», а потом отфильтровать | Перевыгружать выборку заново вместо курсора |
Обе ошибки из последней строки стоят одинаково — денег за объекты, которые вам не нужны или которые вы уже получали. Сужайте фильтр до выгрузки и продолжайте ленту курсором вместо повторного прохода.
Почему пагинация только курсорная
Смещения (offset) в API нет, и это осознанное решение. Данные меняются во время выгрузки: пока вы читаете десятую страницу, в начало выборки добавляются новые закупки — при постраничном чтении по смещению часть объектов вы получите дважды, а часть пропустите.
Курсор описывает позицию в потоке, а не номер строки, поэтому выгрузка остаётся консистентной, а тот же самый курсор потом продолжает поток изменений. Один механизм решает обе задачи — выгрузить и следить.
- Размер страницы —
limit, до 100 объектов (по умолчанию 100 в лентах). - Объектов в сутки — без лимита, расход ограничен балансом.
- Частота — 100 запросов в минуту на аккаунт, 20 в секунду по IP.
- Ответ
409 cursor_expired— лента пересобрана; свежий срез и новый курсор придут в теле ответа.
Пример: выгрузка в свою базу
import os, time, requests
API = "https://public.fraim.ru/api/v2"
H = {"Authorization": f"Bearer {os.environ['FRAIM_TOKEN']}"}
FILTER = {
"keywords": ["асфальт", "дорожные работы"],
"exception_keywords": ["проектирование"],
"regions": [1, 2],
"price": {"from": 500000},
"limit": 100,
}
def dump():
cursor, seen = None, set()
while True:
body = {"cursor": cursor} if cursor else FILTER
r = requests.post(f"{API}/tenders/query", json=body, headers=H, timeout=60)
if r.status_code == 202: # идёт фоновый сбор — повторить тот же запрос
time.sleep(r.json().get("retry_after", 5))
continue
if r.status_code == 429: # лимит частоты: подождать и продолжить
time.sleep(int(r.headers.get("Retry-After", 60)))
continue
r.raise_for_status()
data = r.json()
for obj in data["results"]:
tender = obj["tender"] # объект лежит внутри конверта события
key = (tender["id"], obj["version"]) # дедупликация — по паре id + version
if key not in seen:
seen.add(key)
save(tender) # ваша запись в БД
cursor = data["next_cursor"]
if not data["results"]: # выборка исчерпана
return cursor # курсор сохраните: с него пойдут измененияВозвращённый курсор — это и есть точка, с которой дальше поедут обновления. Хранить нужно только его, а не всю историю запросов.
Поддержание базы в актуальном состоянии
Закупка живёт: меняются сроки, документация, статус. Правильный способ это ловить — продолжать ленту с сохранённого курсора по расписанию, а не выгружать выборку заново.
curl -X POST https://public.fraim.ru/api/v2/tenders/query \
-H "Authorization: Bearer $FRAIM_TOKEN" \
-H "Content-Type: application/json" \
-d '{"cursor": "cur_eyJ…", "limit": 100}' Пустой results с непустым next_cursor означает «сейчас изменений нет» — лента продолжается, курсор остаётся рабочим. Такой опрос бесплатен: списываются только фактически доставленные новые версии объектов.
Документы закупок в выгрузке
У каждого документа закупки и контракта есть постоянная ссылка: без токена, без срока годности, файл идёт через наши сервера с корректным именем. Скачивание входит в цену объекта — отдельной наценки за документы нет.
Карточка объекта в выдаче поиска приходит целиком; отдельный запрос GET /tenders/{id} нужен только ради позиций, документов и лотов.
Как посчитать стоимость выгрузки
Стоимость считается по объектам, а не по запросам: от 0,75 ₽ за доставленный объект. Цена объекта зависит от суммы разового пополнения баланса: чем больше пополнение, тем ниже цена. Тендеры и контракты стоят одинаково.
- Первая доставка версии объекта — платная.
- Повторная выдача той же версии — бесплатна: ретраи, перечитывание курсора и пустые ответы баланс не тратят.
- Купленные объекты не сгорают, в отличие от пакетов с периодом действия.
- Абонплаты и годовых лицензий нет — вход стоит 0 ₽.
Практический вывод для планирования: считайте не «сколько всего закупок в стране», а сколько объектов реально попадёт под ваш фильтр в месяц. Сужение фильтра — самый быстрый способ снизить счёт, а поиск по ИНН и фильтр по типу закупки сужают его сильнее всего.