База тендеров

База тендеров и госзакупок: как собрать её у себя

Выгрузка закупок в свою базу делается курсорной пагинацией: POST /tenders/query с фильтром, затем повторные запросы с полученным курсором — до 100 объектов на страницу, без лимита на объём в сутки. Главное решение принимается до первого запроса: нужен разовый дамп или база, которую придётся держать актуальной. От этого зависит и архитектура, и стоимость.

Разовый дамп или актуальная база

За «базой тендеров» приходят с двумя разными задачами, и путать их дорого.

Разовый дампАктуальная база
ЗачемАналитика за период, обучение модели, оценка рынкаПродукт, мониторинг, CRM, скоринг
Как читатьКурсор до конца выборки один разКурсор сохраняется и продолжается по расписанию
РасходРазовый, предсказуемыйНебольшой постоянный: платятся только изменения
Частая ошибкаВыгрузить «всё», а потом отфильтроватьПеревыгружать выборку заново вместо курсора

Обе ошибки из последней строки стоят одинаково — денег за объекты, которые вам не нужны или которые вы уже получали. Сужайте фильтр до выгрузки и продолжайте ленту курсором вместо повторного прохода.

Почему пагинация только курсорная

Смещения (offset) в API нет, и это осознанное решение. Данные меняются во время выгрузки: пока вы читаете десятую страницу, в начало выборки добавляются новые закупки — при постраничном чтении по смещению часть объектов вы получите дважды, а часть пропустите.

Курсор описывает позицию в потоке, а не номер строки, поэтому выгрузка остаётся консистентной, а тот же самый курсор потом продолжает поток изменений. Один механизм решает обе задачи — выгрузить и следить.

  • Размер страницы — limit, до 100 объектов (по умолчанию 100 в лентах).
  • Объектов в сутки — без лимита, расход ограничен балансом.
  • Частота — 100 запросов в минуту на аккаунт, 20 в секунду по IP.
  • Ответ 409 cursor_expired — лента пересобрана; свежий срез и новый курсор придут в теле ответа.

Пример: выгрузка в свою базу

Python
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 ₽.

Практический вывод для планирования: считайте не «сколько всего закупок в стране», а сколько объектов реально попадёт под ваш фильтр в месяц. Сужение фильтра — самый быстрый способ снизить счёт, а поиск по ИНН и фильтр по типу закупки сужают его сильнее всего.

Обновлено 27 августа 2026 г.. Цифры сверяются с документацией API при каждом изменении.
Вопросы

Вопросы о базе тендеров и выгрузке

Как выгрузить базу тендеров 44-ФЗ целиком?

Через курсорную выгрузку: POST /tenders/query с нужным фильтром, затем повторные запросы с полученным next_cursor, пока приходят результаты. Страница — до 100 объектов. Но выгрузка «всего 44-ФЗ» почти никогда не нужна и стоит дорого: платите вы за доставленные объекты, поэтому фильтр по регионам, ценам и ключевым словам окупается сразу.

Есть ли лимит на объём выгрузки в сутки?

Лимита на число объектов в сутки нет — расход ограничен только балансом. Ограничены частота запросов (100 в минуту на аккаунт, 20 в секунду по IP) и размер страницы (до 100 объектов).

Как поддерживать выгруженную базу в актуальном состоянии?

Не перевыгружать её заново, а продолжать ленту по сохранённому курсору: сервер отдаст только новые и изменившиеся закупки по тому же фильтру. Для мониторинга достаточно хранить один последний курсор. Полная перевыгрузка — это повторная оплата тех же объектов новых версий, курсор её избегает.

Что делать при ответе 202 preparing?

Это не ошибка, а фоновый сбор: повторите тот же запрос с теми же аргументами через указанное в ответе время. Пустой results с непустым next_cursor — тоже не конец ленты, а «сейчас подходящих объектов нет».

Как не заплатить дважды за один объект при повторной выгрузке?

Списание идёт за первую доставку конкретной версии объекта. Повторы той же версии бесплатны, поэтому ретраи и перечитывание курсора баланс не тратят. На своей стороне дедуплицируйте по паре id + version — version меняется, когда закупка действительно изменилась.

Проверьте на своих данных

Регистрация — почта и пароль, меньше минуты. Тестовый токен выдаётся сразу: без телефона, звонков менеджера и карты.

Получить тестовый токенДокументация