Начало

Fraim.ru Tenders API v2

Публичный API тендеров и контрактов РФ для сервер-сервер интеграций. Один эндпоинт закрывает поиск, подписку на изменения и выгрузку; остальное — детали реализации, которые сервис берёт на себя.

Только с сервера. API-токен — это доступ к вашему балансу, храните его только на сервере. CORS отключён, доступа из браузера нет. Нужен показ данных в браузере — ходите в API со своего бэкенда, токен держите на сервере.

Модель в одном абзаце

Фильтр определяет множество закупок; тендеры и контракты — две ленты (проекции) одного фильтра. Фильтр создаётся неявно любым запросом и идентифицируется хешем нормализованных параметров. У каждой ленты — потоки событий created и updated, читаемые независимо через курсоры. Курсоры идут по оси индексации, а не по доменным датам — поэтому «отставшие» объекты не теряются. Биллинг привязан к первой доставке версии объекта, а не к запросам.

Базовый адрес

https://public.fraim.ru/api/v2

Что дальше

Начало

Быстрый старт

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

1. Получите токен

В личном кабинете, на странице «API-токены». Секрет показывается один раз — сразу положите его в переменную окружения на сервере.

Не берите токен из браузера: сессия личного кабинета живёт 30 дней, и интеграция на ней однажды встанет. Токен для кода бессрочный и создаётся отдельно — POST /auth/tokens или кнопкой в кабинете.

2. Найдите активные тендеры

curl -X POST https://public.fraim.ru/api/v2/tenders/query \
  -H "Authorization: Bearer $FRAIM_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{"keywords":["ремонт кровли","асфальт"],
       "regions":[1],"status":["active"],"limit":50}'
Про regions: это внутренние id Fraim.ru, а не ОКАТО и не коды на автомобильных номерах. Москва — 1, Воронежская область — 3, а 77 — Республика Бурятия. Коды всех четырёх параметров — в разделе Справочники кодов.

Ответ несёт filter_id, страницу results и next_cursor. Каждый элемент — конверт события с целым объектом внутри:

{
  "filter_id": "flt_6a40ebde3711b9b2",
  "state": "ready",
  "results": [
    { "event": "created", "version": 1, "billed": true,
      "tender": {
        "id": 4821337,
        "etp_id": "0173200001425000456",   // бывает null
        "name": "Ремонт кровли школы №12",
        "price": 4850000.0,
        "publish_date": "2026-08-01T09:00:00Z",
        "start_date": "2026-08-01T09:00:00Z",
        "end_date": "2026-08-14T23:59:00Z",
        "update_date": "2026-08-05T12:30:00Z",
        "end_date_computed": false,
        "positions_inserted": 10,   // позиций у закупки
        "source_url": "https://zakupki.gov.ru/…",
        "region": { "id": 1, "name": "Москва" },
        "platform": { "id": 3, "name": "РТС-тендер" },
        "placement_type": { "id": 7, "name": "Электронный аукцион" },
        "purchase_type": { "id": 1, "name": "Закупка по 44ФЗ" },
        "status": { "id": 1, "name": "Подача заявок" },
        "organization": { "name": "ГБОУ Школа №12", "inn": "7707083893",
                          "kpp": "770701001", "ogrn": "1037739…" }
      } }
  ],
  "next_cursor": "cur_eyJ…",
  "total": 5000,      // справочно, бывает null
  "billed": { "created": 50, "updated": 0, "free": 0,
               "period_total": 50, "balance_exhausted": false }
}
Запрашивать GET /tenders/{id} на каждый элемент ленты не нужно — и не нужно платить за это. В results[].tender объект уже целиком. Карточка добавляет только то, чего в списке нет: позиции, документы, лоты и полные данные организации (адрес, контакты). У контрактов так же: в results[].contract — целый объект с поставщиками и родительской закупкой.

3. Продолжите ленту курсором

Следующую страницу берите тем же запросом, добавив cursor. Курсор несёт filter_id внутри — параметры повторять не нужно:

curl -X POST https://public.fraim.ru/api/v2/tenders/query \
  -H "Authorization: Bearer $FRAIM_TOKEN" \
  -d '{"cursor":"cur_eyJ…","limit":50}'
Тест → продакшн: меняется только токен — fraim_test_…fraim_live_…. Код остаётся прежним.
Начало

Аутентификация

Все маршруты требуют токен в заголовке Authorization:

Authorization: Bearer fraim_live_…
Про legacy X-API-Token: заголовок ещё принимается для боевых токенов, но с ним не работает песочница — переключение тест/бой определяется только по Authorization, и тестовый токен уйдёт на боевой бэкенд и получит 401. Используйте Bearer.

API-токен вечный — живёт до явного удаления. Создаётся в кабинете или запросом POST /auth/tokens. Секрет показывается один раз при создании — в списке токенов вы видите только маскированный token_preview. Активных токенов может быть до 20; на 21-м приходит 409 api_key_limit_reached.

Вход в кабинет — это не токен

Вход в личный кабинет даёт не API-токен, а сессию: JWT на 30 дней, для браузера. В списке /auth/tokens её нет, отзывать её нечем, и по истечении срока она перестаёт работать — 401 invalid_session. Для интеграции нужен именно API-токен: он не привязан к вашему входу, живёт до явного отзыва и не зависит от того, кто и когда заходил в кабинет.

Лимит запросов считается на аккаунт, а не на токен, — выпуск дополнительных токенов частоту не увеличивает. Токены нужны, чтобы разделять доступ по местам использования и отзывать их поштучно.

Тестовый и боевой

Тестовый токен (fraim_test_…) работает в песочнице на демо-данных и не списывает баланс. Боевой (fraim_live_…) обращается к реальным данным и тарифицируется. Держите токен только на сервере.

Концепции

Фильтры, ленты и подписки

Фильтр — сохранённый запрос, определяющий множество закупок. Создаётся неявно любым QUERY-запросом; filter_id = детерминированный хеш нормализованных параметров. Одинаковые по смыслу параметры (другой порядок ключей, регистр, дубли) → всегда один и тот же фильтр.

Лента (feed) — поток событий по фильтру. У фильтра две ленты: тендерная и контрактная. Каждое событие — created или updated, несёт версию объекта.

Подписка — связка пользователь↔фильтр с TTL. Продлевается любым чтением; параметр events выбирает читаемые потоки. Подробнее — Подписки и TTL.

Как работают ключевые слова

Поиск идёт не по подстроке, а по нормализованным словам: текст разбивается на слова и приводится к начальной форме, поэтому «поставка насосов» находит «поставку насоса». Многословный элемент keywords — это фраза (слова должны встретиться вместе), разные элементы списка — ИЛИ.

Слово короче 3 символов в поиске не участвует. Вместе с ним выбрасываются годы (2024, 2025 — любой год 2000–2099) и служебные части речи (предлоги, союзы, частицы).

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

Что это
Примеры — ищутся как есть
Коды с буквами
ГОСТ 12345-78ГОСТ Р 52948-200838-09/зкМ-24-10Ф10.5-25w-40
Число + дефис или слэш + число
123-4562024-2025123/456220401/123
Артикулы с несколькими дефисами
12312-12312-вывABC-123-XYZ
Размеры с x / х
12.5x2010x20x30 мм
Обозначения с подчёркиванием
abc_123файл_2024
Смешанные буквенно-цифровые
ДИТ-2024-ГПприказ-123SN123456S/N12345678abc123123asd
Дефисные цепочки
8-800-555-35-35

Из этого не следует, что стоит сужать поиск по конкретным номерам и обозначениям (ГОСТ 32144-2013, ТУ 27.32.13-001-12345678-2022) — такие коды обычно встречаются только в приложенной документации к закупке, а не в тексте самой карточки, и по ним почти ничего не найдётся. Лучше работают широкие ключевые слова (кабель силовой, электротехническая продукция) с последующим анализом найденных тендеров (например, через ИИ) и, при необходимости, exception_keywords для отсева лишнего.

  • "keywords": ["насос жд"] → ищется насос: короткое слово из фразы выпадает.
  • "keywords": ["насос 2025"] → ищется насос: год выпадает.
  • "keywords": ["насос", "жд"] → ищется насос: второй элемент пустой и игнорируется.
  • "keywords": ["жд"] → искать нечего, 400 «no searchable words».
  • "exception_keywords": ["жд"] → ничего не исключается.

Всё отброшенное перечислено в поле ignored_keywords ответа ([{keyword, reason}]) — по нему видно, что осталось от запроса. Короткие обозначения (жд, кс, ip) в фильтр закладывать бессмысленно: ищите по полному слову (железнодорожный) или отбирайте такие закупки у себя после выдачи.

Слишком общий запрос не принимается

Фильтр — это подписка на ленту, а не разовая выгрузка реестра. Запрос, под который подходит половина закупок страны, даёт нечитаемую ленту, поэтому действуют четыре ограничения.

  • keywords обязателен. {"status":["active"],"regions":[1]}400. Исключение — множество, заданное узко другим способом: ids, list_id, supplier или любое поле customer. «Все активные закупки Москвы» фильтром быть не может, «все закупки заказчика по ИНН» — может.
  • Слишком общие слова отбрасываются. поставка встречается у 41.8% активных закупок, лот — у 19.6%, услуга — у 17.1%: как фильтр они не сужают ничего. Отбрасывается только элемент из одного слова: ["поставка труб стальных"] ищется целиком.
  • В минус-словах не отбрасывается ничего. exception_keywords: ["поставка"] работает как написано: «услуги, но не поставки» — осмысленный запрос.
  • Если после этого искать нечего — 400 «query is too general», а не пустая выдача.
Список общих слов целиком

Сверяется по начальной форме: «поставки», «поставку», «поставок» — это одно слово.

адрес, вид, выполнение, год, закупка, заявка, использование, количество, лот, назначение, наличие, номер, нужда, обеспечение, область, общий, объем, оказание, описание, определение, поставка, применение, проведение, работа, размер, соответствие, тип, услуга, форма, часть, являться

Чистка происходит до вычисления filter_id, поэтому ["поставка","насос"] и ["насос"] — один и тот же фильтр: одна подписка вместо двух. На стоимость это не влияет: платите вы за первую доставку версии объекта, и один и тот же тендер не тарифицируется дважды, даже если подходит под несколько ваших фильтров. И ещё: exception_keywords без непустых keywords400; раньше такой запрос принимался, но минус-слова молча не применялись.

Потоки created и updated

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

По умолчанию (без параметра events) вы получаете только created — поток «новые тендеры по моей теме». Чтобы следить и за изменениями, укажите events: ["created","updated"] в первом запросе: курсор запоминает выбранные потоки, и менять events при чтении с курсором нельзя — значение из тела игнорируется. Потоки читаются раздельно, под разные пайплайны (новые → скоринг/уведомления, изменения → апдейт своей БД), и у каждого свой курсор.

Поле event в конверте каждого результата считается относительно вас: created — вы видите объект впервые, updated — вы уже получали его раньше (возможно, через другой фильтр или карточку).

Ось порядка

Курсоры идут по оси индексации (момент обнаружения у нас), а не по доменным датам. Тендер, опубликованный неделю назад, но проиндексированный сегодня, получает свежую позицию и доезжает следующим опросом — «отставший» объект не проваливается за курсор. Порядок живой ленты — порядок обнаружения; хронологию сортируйте у себя.

Первый запрос и продолжение

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

Хранить filter_id и параметры не нужно — достаточно последнего курсора: он несёт фильтр внутри себя. Следующую страницу можно запросить телом { "cursor": "cur_…" } вообще без других полей.
Концепции

Как читать выдачу

Выдача — живой список, а не зафиксированный снимок. Ниже — следствия, из-за которых интеграции чаще всего считают нормальное поведение багом.

Выдача всегда актуальна

Каждая страница отражает текущее состояние тендеров на момент запроса. Если фильтр задан со status: ["active"], в выдаче будут только тендеры, активные прямо сейчас: тендер, который завершился после попадания в вашу подписку, из выдачи исчезает автоматически. За такие тендеры баланс не списывается.

Страницы не повторяются байт-в-байт

Повторный запрос с тем же курсором может вернуть меньше объектов (часть перестала подходить под фильтр) или те же объекты с обновлёнными данными. Это нормальное поведение живой ленты, а не ошибка. Дедупликацию стройте на паре id + version, а не на составе страницы.

Пустая страница — валидный ответ

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

total — справочное поле

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

Число живое: оно может измениться между двумя вашими запросами, в том числе прямо во время листания. Гарантии «показали 693 — вытащите ровно 693» нет.

total: nullнормальный ответ, а не ошибка, и клиент должен его переживать. Так бывает в четырёх случаях:

Фильтр слишком тяжёлый
Например, «все завершённые» — миллионы записей
Лента ещё собирается
state: preparing или partial
Поток updated
Конечного числа там не существует
Ещё не посчитано
Фоновый пересчёт до фильтра пока не дошёл

Исключение — точное число. Если множество задано перечислением ids или сохранённым списком (list_id), total точный: это размер списка.

Конец выборки по total не определяют. Конец — это пустой results в ответе по курсору. И не пустой next_cursor: он не бывает пустым никогда, потому что лента живая и по тому же курсору позже придут новые тендеры.
Концепции

Подписки и TTL

Подписка — это ваша позиция в ленте фильтра. Она живёт, пока вы её читаете.

  • Подписка на фильтр продлевается любым чтением и живёт 14 дней от последнего запроса.
  • Новая подписка первые сутки живёт в пробном режиме: если вы не повторили запрос в течение 24 часов, она удаляется. Первый же запрос спустя 3+ часа после создания переводит её на полный срок (14 дней).
Опрашивайте фильтр не реже раза в сутки. Если подписка умерла, ваш курсор вернёт 409 со свежим срезом — события за пропущенный период будут недоступны.

Досрочно погасить подписку можно через DELETE /filters/{filter_id}; список живых — GET /filters с полем expires_at.

Концепции

Как работают контракты

Контракты ищутся через закупки (тендеры), по тематике вашего фильтра: ключевые слова, регионы, заказчик и т.д. Сначала определяется множество закупок (критериями, filter_id, list_id или tender_ids), затем по ним выдаются связанные контракты.

status на контракты не влияет

Параметр status фильтра контрактную выдачу не меняет: можно использовать один и тот же filter_id и для мониторинга активных тендеров, и для получения контрактов по завершившимся закупкам той же темы. Заводить отдельный фильтр «со status: completed» ради контрактов не нужно.

Но filter_id в ответах двух лент разный. Если послать одни и те же критерии в /tenders/query и в /contracts/query, вернутся два разных идентификатора: в контрактный хеш входит status: ["completed"]. Передать тендерный filter_id в контрактный запрос можно и нужно — проекция построится. А вот сравнивать filter_id из двух ответов или использовать его как общий ключ дедупликации нельзя.

Когда появляется контракт

Контракт публикуется площадкой через несколько дней после завершения закупки — до этого он не существует нигде. Как только он появляется у источника, он приезжает в вашу ленту. Мгновенной связки «закупка завершилась → контракт в ленте» не бывает: это ограничение источника, а не выдачи.

Первое обращение может вернуть preparing

Контракты производит фоновый опросчик. Первое обращение к контрактной проекции фильтра активирует её и может вернуть 202 preparing, пока идёт первичный обход. Повторите тот же запрос до готовности — другого протокола ожидания нет.

Свежесть

В ответе контрактной ленты — поле freshness.last_polled_at: честная нижняя граница момента, когда мы последний раз опрашивали источник по этому множеству.

Поиск по поставщику

Поставщик существует только в контракте. Есть два режима по ИНН:

  • Пост-фильтр supplier — сужает выдачу уже заданного множества; в filter_id не входит, повторяйте в каждом запросе.
  • Supplier-подписка{"supplier": {...}} без других способов: контрактный фильтр «мои контракты» / «контракты конкурента». Тендерной проекции у него нет.

Заказчик контракта — это организация закупки, поэтому «контракты по заказчику» = обычные критерии customer.inn.

Концепции

Документы

Файлы закупки и контракта — документация, ТЗ, проект контракта, акты — приходят в карточке объекта (GET /tenders/{id}, GET /contracts/{id}) в массиве documents[]. В ленте (QUERY) документов нет.

{
  "ordinal": 1,
  "title": "Приложение 3. Обоснование НМЦК",
  "extension": "xlsx",
  "link": "https://zakupki.gov.ru/44fz/filestore/…",
  "id": "d6e9f213bc379a60315d1",
  "download_url": "https://downloads.fraim.ru/f/MzQ6ZDZlOWYyMTNiYzM3…"
}

Качайте по download_url, а не по link

link — адрес у первоисточника (ЕИС, электронная площадка, сайт заказчика). Он может быть недоступен, отдать HTML-страницу вместо файла или потребовать входа на площадку. download_url ведёт на наш сервер: файл отдаётся с корректным именем, а недавно скачанный — из нашего кеша, без обращения к источнику. Но сам файл живёт у первоисточника: если он его убрал, скачать не получится ни по одной из ссылок — см. «Когда документа нет».

Два способа скачать

  • Готовая ссылка download_url — постоянная, персональная, без срока годности и без заголовка Authorization. Её можно открыть в браузере, отправить письмом, положить в задачу или в CRM. Ссылка подписана для вашего аккаунта, и срок действия у неё не ограничен — но она ведёт за файлом к первоисточнику, а он свои файлы иногда убирает. Если документ нужен вам надолго, сохраните сам файл, а не ссылку на него.
  • Из кода по токену: GET https://downloads.fraim.ru/d/{document_id} с обычным Authorization: Bearer. Рядом — GET https://downloads.fraim.ru/d/{document_id}/meta: состояние документа (state, filename, size, sha256) без скачивания.

Сколько стоит

Документы объекта, который вам уже доставлен, — бесплатны, сколько угодно раз. Если объект ещё не доставлялся, первое скачивание его документа тарифицируется как доставка объекта (одна единица баланса), дальше все его документы бесплатны. При нулевом балансе — 402 balance_exhausted. Подробнее — Биллинг.

Когда документа нет

Файлы хранятся у первоисточника, и он их иногда убирает. Чтобы вы понимали, стоит ли повторять, ответы разные:

  • 410 document_gone — файл удалён у источника, повторять бессмысленно;
  • 409 document_restricted_at_source — площадка отдаёт файл только своим участникам;
  • 409 document_not_a_file — по ссылке источника сейчас страница, а не файл;
  • 503 document_temporarily_unavailable — источник недоступен, повторите по Retry-After;
  • 413 document_too_large — документ больше допустимого размера.

Ни один из этих ответов не списывает баланс. В песочнице скачивание недоступно (409 document_unavailable_in_sandbox): там демо-данные, а файлы лежат у боевых источников.

Концепции

Биллинг

Модель — чистый баланс: доступ даёт токен, оплата пообъектная. Балансы раздельные по типам: tenders и contracts. Лицензий и суточных лимитов нет.

Правила

  • Тарифицируется первая доставка версии объекта, а не запрос. Пустые ответы и поллинг бесплатны.
  • Платны оба события: created и updated (каждая новая версия).
  • Оплата постранично: нашли 5000, забрали 2 страницы по 50 → заплатили за 100. Глубину пагинации контролируете вы.
  • Повторное получение того же объекта — бесплатно (billed: false): тот же фильтр, другой фильтр, карточка, повтор курсора.
  • Тендеры, исключённые из выдачи из-за смены статуса, не списываются.
  • Дедуп между фильтрами: объект под двумя вашими фильтрами тарифицируется один раз.
  • Карточка GET /{id}: списание только если версия ещё не доставлялась (общий учёт с лентами).
  • Поле total — справочная фоновая оценка «сколько сейчас подходит под фильтр», а не счётчик оплаченного и не размер выборки; бывает null. Планировать по нему расходы нельзя: платите вы за то, что реально пролистали. Подробнее — Как читать выдачу.

Поле billed

billed — это про деньги, не про «видел ли я объект». Видел ли — определяйте по своей базе (id + version). Повторная доставка той же версии в любом потоке — billed: false, бесплатно.

Недостаточный баланс

Страница отдаётся частично: доставлено и списано то, на что хватило, billed.balance_exhausted: true, а next_cursor указывает перед первым неоплаченным. HTTP 200 при частичной выдаче, 402 при нуле доставленных. После пополнения — продолжение с того же места без потерь и дублей.

Журнал

GET /balance/ledger — по строке на тарифицированную доставку: объект, версия, фильтр, request_id. Любое списание объяснимо построчно.

Концепции

Курсоры и надёжность

Курсоры

Курсор — непрозрачная подписанная строка cur_<payload>.<sig>: позиция в конкретной ленте для конкретного набора событий. Возвращайте как есть. В ответе возвращается только next_cursor (одно поле = одна инструкция «сохрани это»). Невалидная подпись → 400 invalid_cursor.

Только курсоры — никаких page/page_size. До двух курсоров на ленту: по одному на поток created/updated.

Что гарантировано

  • Каждое событие ленты доставляется не более одного раза. Повтор запроса со старым курсором не приводит ни к дублям, ни к повторным списаниям.
  • События не теряются: всё, что появилось в ленте после вашей позиции, будет выдано при следующих чтениях — независимо от того, как часто вы опрашиваете (в пределах срока жизни подписки).
  • Тендер, который вошёл в фильтр и вышел из него между вашими опросами (например, успел завершиться), не доставляется — на него уже нельзя отреагировать.
  • 402 balance_exhausted — пополните баланс и повторите запрос с тем же курсором: выдача продолжится с первого неоплаченного объекта.

Идемпотентность

QUERY идемпотентен по построению — ретрайте свободно. На мутациях (создание токена, операции со списками, снятие подписки) заголовок Idempotency-Key обязателен; без него — 400 idempotency_key_required. Повтор с тем же ключом возвращает сохранённый ответ первого выполнения — те же поля и значения. Побайтового совпадения не ждите: порядок ключей в JSON может отличаться, поэтому сверять ответ по хешу тела или по подписи нельзя.

Обратная ситуация — тот же Idempotency-Key с другим телом — это 409 idempotency_key_reused_with_different_payload (тип idempotency_error). Ключ генерируется один раз на логическую операцию и не переиспользуется для следующей.

Пересборка ленты

Истечение подписки — не ошибка: следующий запрос вернёт preparing → пересборка → продолжение. Если продолжение без потерь невозможно, придёт 409 cursor_expired: лента была пересобрана, в теле уже лежит свежий срез и новый курсор — продолжайте с него, повторный запрос не нужен (повторные объекты бесплатны). Читайте их из detail: detail.results и detail.next_cursor, а не с верхнего уровня — см. формат ошибок. История за период вашего отсутствия недоступна.

Деградация под нагрузкой

Конкурентность тяжёлых поисков ограничена, но отказов нет: под пиком новый фильтр вернёт 202 preparing, а total/freshness могут отсутствовать (null). Правило одно: получил preparing — повтори тот же запрос. Ошибок 429/5xx из-за конкуренции поисков не бывает.

В теле 202 preparing есть два поля: retry_after — через сколько секунд повторить запрос, и progress — доля собранной ленты от 0 до 1 (для прогресс-индикатора, логику на нём строить не нужно). Ответ 200 со state: "partial" означает «часть ленты уже доступна, читайте курсором — продолжение доедет».

Справочник

Эндпоинты

17 публичных маршрутов. Все требуют токен. QUERY — это POST /…/query с телом JSON.

Auth токены доступа

GET/auth/tokensсписок токенов

Все токены пользователя с маскированным превью — без секретов.

GET /auth/tokens

→ 200
{ "tokens": [ { "id": 12, "name": "Prod",
     "token_preview": "064c...3b80", "is_active": true,
     "created_at": "…", "last_used_at": null } ],
  "total": 1 }
POST/auth/tokensсоздать токен

Создаёт API-токен — сессией кабинета или уже имеющимся токеном. Требует Idempotency-Key. Секрет — в ответе один раз. Активных токенов не больше 20: на 21-м приходит 409 api_key_limit_reached, отзовите ненужный.

Параметры
Idempotency-Key header, string
Обязателен на мутациях
name body, string
Название токена, 1–100 символов
POST /auth/tokens
Idempotency-Key: 3f2a…
{ "name": "CRM интеграция" }

→ 200  { "id": 13, "token": "fraim_live_…" }
DELETE/auth/tokens/{token_id}отозвать

Деактивирует токен. Требует Idempotency-Key. Можно удалять только свои токены.

Параметры
token_id path, integer
ID токена
Idempotency-Key header, string
Обязателен
DELETE /auth/tokens/13
Idempotency-Key: 88ac…

→ 200  { "success": true, "message": "revoked" }

Tenders поиск и лента

QUERY/tenders/queryпоиск и лента

Центральный эндпоинт. Ровно один способ задать множество: критерии поиска (включая ids) | filter_id | list_id. Смешение → 400. Продолжение ленты — курсором (несёт filter_id внутри) или тем же filter_id с начала. Выдача всегда актуальна: страница отражает состояние тендеров на момент запроса, поэтому повторный запрос с тем же курсором может вернуть меньше объектов или те же объекты с обновлёнными данными. Пустой results с next_cursor — норма, а не конец ленты. Поле total справочное: фоновая оценка «сколько сейчас подходит под фильтр», меняется между запросами и бывает null — конец выборки определяется пустым results, а не total. Каждое событие доставляется не более одного раза, повтор со старым курсором не даёт ни дублей, ни повторных списаний. Слишком общий запрос не принимается: keywords обязателен (кроме ids/list_id/supplier/customer), одиночные общие слова отбрасываются и перечисляются в ignored_keywords, а если после этого искать нечего — 400.

Параметры
keywords string[]
Ключевые слова/фразы, ИЛИ-семантика; многословный элемент — как фраза. ОБЯЗАТЕЛЕН, кроме фильтров по ids/list_id/supplier/customer. Слова короче 3 символов, годы и предлоги игнорируются; одиночные слишком общие слова (поставка, услуга, работа, лот…) отбрасываются — см. ignored_keywords в ответе. Если искать нечего → 400
exception_keywords string[]
Минус-слова: исключить закупки с ними. Только вместе с непустым keywords, иначе 400. Слова короче 3 символов игнорируются; слишком общие слова здесь НЕ отбрасываются
sections integer[]
Где искать слова: 1 закупка, 2 лот, 3 позиция, 4 характеристики позиции. По умолчанию везде; [1,2,3] — не искать по характеристикам
regions integer[]
ID регионов. Внутренние id Fraim.ru, коды — в разделе «Справочники кодов»
platforms integer[]
ID площадок (ЭТП)
price {from,to}
Начальная цена закупки (НМЦК)
advance {from,to}
Размер аванса по контракту, % от цены (0–100). Не обеспечение заявки и не обеспечение контракта. 0 — аванса нет или он не указан; заполняется только у закупок из ЕИС. {"from": 1} — только с авансом, {"to": 0} — без аванса
publish_date {from,to}
Дата публикации, ISO-даты
start_date {from,to}
Дата начала подачи заявок, ISO-даты
end_date {from,to}
Дата окончания подачи заявок, ISO-даты
placement_types integer[]
ID способов размещения. Внутренние id Fraim.ru, коды — в разделе «Справочники кодов»
purchase_types integer[]
ID типов закупки. Внутренние id Fraim.ru, коды — в разделе «Справочники кодов»
customer {inn[],ogrn,name}
Заказчик закупки
status string[]
active | completed | cancelled; дефолт ["active"]. Выдача живая: завершившийся тендер исчезает из неё сам и не тарифицируется
ids integer[]
PK-lookup, ≤1000; любые статусы, синхронно
filter_id / list_id string
Альтернативные способы задать множество
events string[]
created — объект впервые появился в вашем фильтре, updated — изменился уже бывший в нём. Дефолт ["created"]. Указывается в ПЕРВОМ запросе: набор потоков запоминает курсор, дальше значение из тела игнорируется
cursor string
Продолжение ленты; несёт filter_id внутри — можно прислать один, без других полей
limit integer
1–100
лишние поля body
Незнакомое поле ВЕРХНЕГО уровня (page, offset, опечатка region вместо regions) игнорируется молча, без ошибки. Строго проверяются только вложенные customer и supplier: незнакомое поле там → 422. Сверяйте applied в ответе
QUERY /tenders/query
{ "keywords": ["асфальт","укладка асфальта"],
  "exception_keywords": ["ямочный"],
  "regions": [1],
  "price": {"from": 100000, "to": 5000000},
  "customer": {"inn": ["7707083893"]},
  "status": ["active"], "events": ["created"], "limit": 100 }

→ 200 { "filter_id","state":"ready","total":5000,
        "results":[{ "event":"created","version":1,"billed":true,
                     "tender":{ /* объект целиком: name, price,
                                  region, platform, status,
                                  organization, даты */ } }],
        "next_cursor":"cur_…",
        "billed":{"created":100,"updated":0,"free":0,
                   "period_total":100,"balance_exhausted":false} }
GET/tenders/{id}карточка тендера

Ответ завёрнут в тот же конверт события, что и элемент ленты: { event, version, billed, tender }. Карточка богаче элемента ленты ровно на то, чего в списке нет: лоты, позиции (постранично, с ОКПД2/КТРУ), документы, полная организация (адрес, контакты администратора), placement_type/purchase_type, purchase_plan_id. Всё остальное уже пришло в ленте — за карточкой ради name или price ходить не нужно. Списание — только если версия ещё не доставлялась.

Параметры
id path
Внутренний id или etp_id
id_type query
internal (по умолчанию) | external
positions_page query
Страница позиций, по умолч. 1
positions_limit query
Размер страницы позиций, 1–100, по умолч. 20
GET /tenders/4821337?positions_page=1&positions_limit=20

→ 200
{ "event":"created", "version":1, "billed":false,
  "tender": {
    "id":4821337, "etp_id":"0173…", "name":"…", "price":4850000.0,
    "region":{…}, "platform":{…}, "status":{…},
    "organization":{ "name","inn","kpp","ogrn","address",
                     "administrator":{"name","phone","email"} },  // бывает null
    "lots":[{"ordinal":1,"title":"…","price":4850000.0}],
    "positions":[{ "lot_ordinal":1, "position_ordinal":1,
                   "title":"…", "characteristics":"",
                   "quantity":1.0, "unit":"шт",
                   "price":35000.0, "amount":35000.0,
                   "okpd2":{"code":null,"name":null},  // объект даже без кода
                   "ktru":null }],                      // бывает null
    "positions_total":54, "positions_page":1,
    "positions_page_size":20, "positions_pages":3,
    "documents":[{"title","link","extension"}],
    "documents_total":7 } }

Contracts контракты по закупкам

QUERY/contracts/queryконтракты по множеству

Контракты ищутся через закупки (тендеры) — по тематике вашего фильтра: ключевые слова, регионы, заказчик и т.д. Ровно один способ задать множество: критерии закупок | filter_id | list_id | tender_ids | supplier. Параметр status фильтра на контрактную выдачу НЕ влияет: один и тот же filter_id годится и для мониторинга активных тендеров, и для получения контрактов по завершившимся закупкам той же темы — отдельный фильтр «со status completed» заводить не нужно. supplier — пост-фильтр по поставщику ИЛИ отдельная supplier-подписка.

Параметры
keywords, regions, …
Те же критерии закупок, что у /tenders, без ids; status игнорируется. Правила ключевых слов те же: keywords обязателен, слишком общие одиночные слова отбрасываются
tender_ids integer[]
Разовое множество закупок (заморожено)
supplier {inn[],name}
Пост-фильтр или supplier-подписка по поставщику контракта
filter_id / list_id string
Контрактная проекция существующего множества
events, cursor, limit
Как у /tenders
// контракты поставщика по ИНН (supplier-подписка)
QUERY /contracts/query
{ "supplier": {"inn": ["7701234567"]} }

// контракты по критериям закупок
{ "keywords": ["асфальт"], "regions": [1] }

→ 202 preparing  → повтор → 200 ready
{ "results":[{ "event":"created","version":1,"billed":true,
    "contract":{ "id":55123, "reestr_number":"…",
      "name":"…", "price":4850000.0, "status":"…",
      "conclusion_date":"…", "execution_date":"…",
      "currency":{"code":"RUB","name":"…"},
      "suppliers":[{"name","inn","kpp","country_code",
                    "address","postal_address","phone",
                    "email","status"}],
      "tender":{"id","etp_id","name"} } }] }   // только 3 поля
GET/contracts/{id}карточка контракта

Как и у тендеров, ответ завёрнут в конверт события: { event, version, billed, contract }. Карточка добавляет позиции (постранично — positions[] плюс positions_total/positions_page/positions_page_size/positions_pages), документы, исполнение, заказчика и версионные поля ЕИС (ikz, version_number, is_changed, changed_fields). id_type: internal | reestr. Списание — только для недоставленной версии.

Параметры
id path
Внутренний id или реестровый номер
id_type query
internal | reestr
positions_page / positions_limit query
Пагинация позиций
GET /contracts/55123?id_type=internal

→ 200 { "event":"created", "version":1, "billed":false,
        "contract": { "id":55123, "suppliers":[…],
          "currency":{"code":"RUB","name":"Российский рубль"},
          "customer":{…}, "execution":{…},
          "positions":[{ "position_ordinal":1, "title":"…",
            "quantity":1.0, "unit":"шт", "price":580.46,
            "amount":580.46, "okpd2":{…}, "ktru":null,
            "nds":"…", "mnn":null, "is_vital_drug":false }],
          "positions_total":2153, "positions_page":1,
          "positions_page_size":20, "positions_pages":108,
          "documents":[…], "documents_total":12,
          "ikz":"…", "plan_position_number":"…",
          "result_date":"…", "penalty_withholding":false,
          "version_number":3, "is_changed":true,
          "changed_fields":{…} } }

Lists изменяемые коллекции закупок

POST/listsсоздать список

Персональная изменяемая коллекция тендеров без TTL. name 1–255, description ≤2000, metadata ≤2048 байт. После создания поля неизменяемы. Живёт до явного удаления через DELETE /lists/{list_id}. PATCH нет — после создания меняется только состав.

Параметры
name body, string
1–255 символов
description body, string
≤2000 символов
metadata body, object
JSON ≤2048 байт, не индексируется
Idempotency-Key header
Обязателен
POST /lists
Idempotency-Key: a1b2…
{ "name": "Проект Юг", "metadata": {"project_id": 42} }

→ 200 { "list_id": "lst_b20d926e2be5636487", … }
DELETE/lists/{list_id}удалить список

Удаляет список вместе с составом и гасит подписку на ленту фильтра-над-списком. Уже доставленные объекты остаются доставленными — повторно за них деньги не списываются. Требует Idempotency-Key.

Параметры
list_id path
ID списка
Idempotency-Key header
Обязателен
DELETE /lists/lst_b20d926e2be5636487
Idempotency-Key: 9f8e…

→ 200 { "list_id": "lst_b20d926e2be5636487", "deleted": true }
GET/listsмои списки

Курсорная пагинация. Конверт коллекции: { data, has_more, next_cursor }.

Параметры
limit query
По умолчанию 20
starting_after query
Курсор
GET /lists?limit=20

→ 200 { "data":[{"list_id":"lst_b20d926e2be5636487","name":"…"}],
        "has_more": false, "next_cursor": null }
GET/lists/{list_id}детали и состав

Метаданные списка + состав в поле items — это вложенный конверт коллекции, а не поля верхнего уровня. items.data — массив tender_id (просто числа), момент добавления не отдаётся.

Параметры
list_id path
ID списка
limit query
По умолчанию 100
starting_after query
Курсор по составу — последний tender_id
GET /lists/lst_b20d926e2be5636487

→ 200 { "list_id":"lst_b20d926e2be5636487", "name":"Проект Юг",
        "description":null, "metadata":{…},
        "created_at":"2026-08-01T09:00:00Z",
        "items": { "data":[4821001, 4821337],
                   "has_more":false, "next_cursor":null } }
POST/lists/{list_id}/itemsдобавить тендеры

Идемпотентно: повторное добавление не создаёт дублей. Добавленный тендер сразу виден через QUERY {list_id}, его контракты доливаются в контрактную ленту. Существование тендеров проверяется: несуществующий id даёт 400 validation_error, и пакет не применяется частично — либо входят все, либо ни одного.

Параметры
list_id path
ID списка
ids body, integer[]
1–10000 tender_id
Idempotency-Key header
Обязателен
POST /lists/lst_b20d926e2be5636487/items
Idempotency-Key: c3d4…
{ "ids": [4821337, 4899210] }
DELETE/lists/{list_id}/itemsубрать тендеры

Идемпотентно. Доставленное остаётся доставленным; будущие события убранного в ленты не попадают.

Параметры
list_id path
ID списка
ids body, integer[]
tender_id к удалению
Idempotency-Key header
Обязателен
DELETE /lists/lst_b20d926e2be5636487/items
Idempotency-Key: e5f6…
{ "ids": [4821337] }

Filters обвязка для отладки

GET/filtersмои подписки

Живые подписки: параметры, expires_at, created_at и флаг contracts_projection. Фильтры создаются только неявно любым QUERY — это отладочная обвязка, не happy path. Подписка живёт 14 дней от последнего чтения и продлевается любым запросом; новая первые сутки работает в пробном режиме и удаляется, если её не перечитали в течение 24 часов.

Параметры
limit query
По умолчанию 20
starting_after query
Курсор
GET /filters

→ 200 { "data":[{ "filter_id":"flt_6a40ebde3711b9b2",
        "params":{…}, "expires_at":"…",
        "created_at":"…",
        "contracts_projection": true }], "has_more":false }
DELETE/filters/{filter_id}погасить подписку

Гасит вашу персональную подписку досрочно. Сам фильтр (общий кеш параметров) не удаляется.

Параметры
filter_id path
ID фильтра
Idempotency-Key header
Обязателен
DELETE /filters/flt_6a40ebde3711b9b2
Idempotency-Key: 77aa…

Balance баланс и списания

GET/balanceбаланс и расход

Баланс по типам (tenders/contracts) и usage_today — тарифицированных доставок за сутки (справочно, не лимит).

GET /balance

→ 200 { "balance": {"tenders": 4312, "contracts": 84},
        "usage_today": {"tenders": 218, "contracts": 17},
        "last_updated": "2026-07-24T11:42:00Z" }
GET/balance/ledgerжурнал списаний

По строке на тарифицированную доставку: объект, версия, фильтр, request_id. Курсорная пагинация.

Параметры
limit query
По умолчанию 100
starting_after query
Курсор
GET /balance/ledger?limit=100

→ 200 { "data":[{ "id":98765, "ts":"2026-08-05T12:30:00Z",
        "balance_type":"tenders", "amount":-1, "event":"created",
        "object_id":4821337, "version":1,
        "filter_id":"flt_…", "request_id":"req_…" }],
        "has_more": true, "next_cursor": 98765 }
Справочник

Справочники кодов

Значения для regions, platforms, purchase_types и placement_types. Это внутренние id Fraim.ru: не ОКАТО, не коды на автомобильных номерах и не коды ЕИС. Москва — 1, Санкт-Петербург — 34, а 77 — Республика Бурятия.

Названия в таблицах — те, что API возвращает в полях region, platform и далее, поэтому по ним же можно сверять ответы. Снимок сверен с боевым API 2026-08-09. Те же таблицы с поиском — в личном кабинете, а для скрипта — одним файлом: /refs.json.
regionsРегионы · 90

Нумерация внутренняя: это НЕ ОКАТО, не коды на автомобильных номерах и не коды ЕИС. Москва — 1, Санкт-Петербург — 34, а 77 — Республика Бурятия.

1Москва
2Московская область
34Санкт-Петербург
35Ленинградская область
3Воронежская область
4Тульская область
5Владимирская область
6Ярославская область
7Рязанская область
8Тверская область
9Брянская область
10Смоленская область
11Белгородская область
12Курская область
13Липецкая область
14Калужская область
15Ивановская область
16Тамбовская область
17Костромская область
18Орловская область
19Республика Башкортостан
20Пермский край
21Самарская область
22Саратовская область
23Республика Татарстан
24Нижегородская область
25Кировская область
26Удмуртская Республика
27Оренбургская область
28Чувашская Республика - Чувашия
29Ульяновская область
30Пензенская область
31Республика Марий Эл
32Республика Мордовия
36Вологодская область
37Республика Коми
38Мурманская область
39Архангельская область
40Калининградская область
41Республика Карелия
42Новгородская область
43Псковская область
44Ненецкий АО
45Новосибирская область
46Красноярский край
47Иркутская область
48Кемеровская область
49Омская область
50Томская область
51Алтайский край
52Республика Хакасия
53Республика Алтай
54Республика Тыва
57Свердловская область
58Ханты-Мансийский Автономный округ - Югра АО
59Челябинская область
60Тюменская область
61Ямало-Ненецкий АО
62Курганская область
63Краснодарский край
64Ростовская область
65Волгоградская область
66Республика Крым
67Астраханская область
68Севастополь
69Республика Адыгея
70Республика Калмыкия
71Республика Саха (Якутия)
72Приморский край
73Хабаровский край
74Забайкальский край
75Сахалинская область
76Амурская область
77Республика Бурятия
78Камчатский край
79Магаданская область
80Чукотский АО
81Еврейская автономная область
84Ставропольский край
85Республика Дагестан
86Чеченская Республика
87Кабардино-Балкарская Республика
88Республика Северная Осетия - Алания
89Карачаево-Черкесская Республика
90Республика Ингушетия
93Байконур (арендован РФ)
94Херсонская область
95Луганская Народная Республика
96Донецкая Народная Республика
97Запорожская область
"regions": [1]
platformsПлощадки (ЭТП) · 26 из нескольких сотен

Здесь наиболее используемые площадки; всего их в индексе несколько сотен. Код нужной площадки всегда виден в поле platform {id, name} любой выдачи.

2РТС–тендер
20Росэлторг
418АО «Сбербанк-АСТ»
13Портал Закупай
110ЭТП ГПБ
8Портал закупок Москвы
39Фабрикант
67ТЭК-Торг
1ЕИС ЗАКУПКИ
98B2B-Center
16223ETP ZakazRF
250БП ZakazRF
12Лот-Онлайн
113Электронный магазин ГЗ СПб
11ТендерПро
5Единый агрегатор торговли «Березка»
106Бидзаар
62Закупки Ленрег
108Сбер B2B
53Торги ЭТП Регион
60Торги82 ЭТП
194ЭТП Р-Эст
386ЭТП Торги Онлайн
421АО «Сбербанк-АСТ» (УТП)
445ЭТП «Торги 223»
446ЭТП ФЕДЕРАЦИЯ ЗАКУПОК
"platforms": [2]
purchase_typesТипы закупки · 5

Закон или режим, по которому проводится закупка.

1Закупка по 44ФЗ
2Закупка по 223ФЗ
3Коммерческая закупка
4Малая закупка
5Закупка по 615ПП
"purchase_types": [1]
placement_typesСпособы размещения · 23

Форма процедуры. Приходит в карточке тендера полем placement_type.

7Электронный аукцион
4Запрос котировок
8Открытый конкурс
2Запрос предложений
5Конкурс
6Аукцион
11Открытый аукцион
13Закупка у единственного поставщика
14Мелкая закупка
9Упрощенная закупка
15Открытый запрос предложений
16Открытый запрос котировок
3Объявление о покупке
10Мониторинг цен
12Маркетинговое исследование
17Предварительный отбор
18Закрытый конкурс
19Конкурентные переговоры
20Закрытый запрос котировок
101Закрытый аукцион
102Закрытый запрос предложений
103Конкурс с ограниченным участием
1Прочее
"placement_types": [7]

Если нужного значения в таблице нет, посмотрите его в самой выдаче: объекты несут region {id, name} и platform {id, name}, а карточка тендера — placement_type и purchase_type. Отдельного маршрута справочников у API нет.

Справочник

Лимиты

Лимиты рассчитаны так, чтобы обычная интеграция их не замечала. Упереться можно в основном при выгрузке в несколько параллельных потоков.

Что
Сколько
Подробности
Запросы к API
100 в минуту
Считается на аккаунт, а не на токен. Это гарантированный минимум: счётчик живёт в памяти процесса, поэтому на практике может пропустить и больше — не стройте на этом расчёт. Сверх — 429 rate_limit_exceeded с Retry-After: 60.
Всплеск запросов
20 в секунду
Отдельный лимит по IP, режет всплески (короткая пачка до 40 запросов проходит). Параллельная выгрузка в несколько потоков упирается в него раньше, чем в минутный. Сверх — 429 с Retry-After: 1: хватает паузы в секунду и меньшего числа потоков.
Объектов в сутки
без лимита
Расход ограничен только балансом.
Активных токенов
до 20
На 21-м — 409 api_key_limit_reached: отзовите ненужный.
Выпуск токенов
200 в час
Повтор с тем же Idempotency-Key за выпуск не считается.
Размер страницы
до 100
Параметр limit. По умолчанию 100 в лентах (QUERY) и 20 в списках подписок и своих списков.
ID в составе списка
до 10 000
Поле ids в POST и DELETE /lists/{list_id}/items. У POST /lists поля ids нет — там только name/description/metadata.
ID в поиске
до 1000
Поле ids в POST /tenders/query (PK-lookup). Сверх — 400 validation_error.
Новых фильтров
до 50
Считаются только неподтверждённые: фильтр, который вы продолжаете читать, из счёта уходит.
Если получили 429: подождите Retry-After и повторите тот же запрос — ничего не потеряно, за отклонённый запрос списаний нет. Заголовок есть у обоих ограничителей: у всплеска это 1 секунда, у минутного лимита — 60. Не подставляйте свою константу: пауза в минуту там, где хватило бы секунды, — это потерянная минута на каждой пачке.

Под пиковой нагрузкой на поиск API не отказывает, а отвечает 202 preparing — см. Курсоры и деградацию. С частотой запросов это не связано: 429 всегда приходит от ограничителя частоты, а не от загруженности поиска.

Справочник

Коды ошибок

Тело ошибки вложено в detail — так его отдаёт FastAPI:

{
  "detail": {
    "error": {
      "type": "invalid_request_error",
      "code": "validation_error",
      "message": "…",
      "param": "cursor",     // поле, из-за которого ошибка, либо null
      "request_id": "req_…"
    }
  }
}

request_id приходит заголовком X-Request-Id в каждом ответе, а в теле ошибок дублируется полем error.request_id. По нему же сходятся строки GET /balance/ledger — указывайте его в обращениях в поддержку (support@fraim.ru). Если пришлёте свой X-Request-Id в запросе, API вернёт именно его: сквозная трассировка работает. Единственный ответ без него — отказ лимитера всплесков (Retry-After: 1): он формируется до приложения, поэтому идентификатора у него нет.

Одно исключение, которое нужно учесть в обработке ошибок. Форма выше одна для всех кодов без исключений, включая 422 и 429. Особый случай один — у 409 cursor_expired внутри detail лежит не только error, но и свежий срез ленты: detail.state, detail.results, detail.next_cursor.
Отдельно про 429. Обе разновидности отвечают этой же формой и несут Retry-After: минутный лимит на аккаунт — 60, лимит всплесков по IP — 1. Второй срабатывает раньше при выгрузке в несколько параллельных потоков и отличается сообщением burst limit exceeded; только у него request_id равен null, потому что отказ формируется до приложения.
HTTP
code
Когда
400
validation_error
Невалидные параметры; ids>1000; >1 способа задать множество
400
invalid_cursor
Подпись курсора не сошлась
400
idempotency_key_required
Мутирующий запрос без заголовка Idempotency-Key
401
api_key_required
Нет заголовка Authorization
401
invalid_api_key
Токен неизвестен, отозван или не похож на ключ Fraim.ru
401
api_key_mode_mismatch
Префикс токена не совпал с его режимом (тестовый против боевого)
401
account_inactive
Аккаунт заблокирован
401
invalid_session
Сессия кабинета истекла (30 дней) — войдите заново; к API-токенам не относится
402
balance_exhausted
Баланс 0 и ни один объект не доставлен
404
not_found
Объект/фильтр/список не существует или чужой список
405
method_not_allowed
Метод не поддержан маршрутом — напр. DELETE /lists (удаления списка нет)
409
cursor_expired
Лента пересобрана; в теле — свежий срез и новый курсор
409
api_key_limit_reached
Уже 20 активных токенов — отзовите ненужный и повторите
409
idempotency_key_reused_with_different_payload
Тот же Idempotency-Key прислан с другим телом. Тип ошибки — idempotency_error
409
document_restricted_at_source
Документ есть, но площадка-первоисточник отдаёт его только своим участникам
409
document_not_a_file
По ссылке первоисточника сейчас страница, а не файл
409
document_unavailable_in_sandbox
Скачивание документов тестовым токеном недоступно
410
document_gone
Документ удалён у первоисточника — повторять бессмысленно
413
document_too_large
Документ больше допустимого размера для скачивания через сервис
503
document_temporarily_unavailable
Первоисточник недоступен; в ответе Retry-After — повторите позже
422
validation_error
Запрос не сошёлся со схемой: limit вне 1–100, metadata > 2048 байт, неверный тип поля
422
extra_forbidden
Незнакомое поле во вложенном фильтре customer или supplier
429
rate_limit_exceeded
Лимит частоты: минутный на аккаунт (Retry-After: 60), всплеск по IP (Retry-After: 1), попытки входа или выпуска токенов. Подождите указанное время и повторите
202
preparing
Не ошибка: идёт фоновый сбор, повторите тот же запрос