Fraim Tenders API V2 — документация для ИИ-агентов
HTTP API по тендерам и контрактам России (44-ФЗ, 223-ФЗ, коммерческие торги): поиск закупок, живые ленты изменений по сохранённому фильтру, карточки тендеров и контрактов, история по ИНН заказчика и поставщика. Только server-to-server, авторизация Bearer-токеном, оплата — за доставленный объект, а не за запрос.
Документ самодостаточен: его можно целиком положить в контекст модели. Справочник эндпоинтов, параметры и схемы сгенерированы из OpenAPI-схемы API — расхождений с боевым поведением нет.
- Base URL:
https://public.fraim.ru/api/v2 - Версия API: 2.0.0 (OpenAPI 3.1.0, снапшот схемы от 2026-08-05)
- Этот документ одним файлом: https://api.fraim.ru/ai.md
- Машинная схема (OpenAPI 3.1): https://public.fraim.ru/api/v2/openapi.json
- Документация для людей: https://api.fraim.ru/docs
- Личный кабинет (токены, баланс, песочница): https://lk.api.fraim.ru
Кратко: правила интеграции
- Ходите в API только с сервера. Токен — это доступ к балансу; CORS отключён намеренно, из браузера API не работает.
- Авторизация — заголовок
Authorization: Bearer <token>. Легаси-заголовокX-API-Tokenне используйте: с ним не работает песочница. - Поиск — это
POST /tenders/queryс JSON-телом, а не GET со строкой запроса. Отдельного/searchнет. - Множество задаётся ровно одним способом: критерии |
filter_id|list_id(у контрактов ещёtender_ids). Смешение →400 validation_error. - Пагинация только курсорная. Возьмите
next_cursorиз ответа и пришлите{"cursor": "cur_…"}— остальные поля повторять не нужно, курсор несёт фильтр внутри. Параметровpage/offsetне существует. 202 preparing— не ошибка: идёт фоновый сбор. Повторите тот же запрос черезretry_afterсекунд.state: "partial"в 200-ответе значит «читать уже можно, лента ещё дособирается».- Тарифицируется первая доставка версии объекта, а не запрос. Лишняя пагинация «на всякий случай» стоит денег; пустые ответы и повторы бесплатны.
- На мутациях (
POST/DELETE) заголовокIdempotency-Keyобязателен — иначе400 idempotency_key_required. Кладите UUID v4 и повторяйте его при ретрае. - При
429дождитесьRetry-Afterи повторите. Лимит считается на аккаунт, а не на токен: заводить новые токены ради частоты бессмысленно. - Не пытайтесь логиниться по email-коду:
/auth/request-codeи/auth/login-code— вход человека в кабинет, они отдают сессию на 30 дней. Агенту нужен вечный API-токен (fraim_live_…) из кабинета илиPOST /auth/tokens. - Дедуплицируйте у себя по паре
id+version. Полеbilledотвечает на вопрос «списали ли деньги», а не «видел ли я этот объект». - Даты — ISO-8601. По умолчанию
status: ["active"]иevents: ["created"]; ленту изменений включайте явно.
Аутентификация
Все маршруты требуют токен:
Authorization: Bearer fraim_live_…
API-токен вечный — живёт до явного отзыва. Создаётся в личном кабинете или запросом POST /auth/tokens; секрет показывается один раз, дальше виден только маскированный token_preview. Активных токенов — до 20.
Два окружения различаются только префиксом токена, адрес и тело запроса те же:
fraim_test_…— песочница на демо-данных, баланс не списывается;fraim_live_…— боевые данные, тарифицируется.
Переключение теста и боя определяется по заголовку Authorization. Легаси-заголовок X-API-Token ещё принимается для боевых токенов, но с ним тестовый токен уйдёт на боевой бэкенд и получит 401 — используйте Bearer.
Маршруты входа человека в кабинет (/auth/request-code, /auth/login-code) в этой документации и в OpenAPI-схеме отсутствуют намеренно: они отдают сессию на 30 дней, для интеграции она не годится.
Модель данных
Четыре понятия, без которых ответы API читаются неправильно.
Фильтр — сохранённый запрос, определяющий множество закупок. Создаётся неявно любым QUERY-запросом; filter_id — детерминированный хеш нормализованных параметров, поэтому одинаковые по смыслу запросы (другой порядок ключей, регистр, дубли) дают один и тот же фильтр.
Лента (feed) — append-only поток событий по фильтру. У фильтра две ленты: тендерная и контрактная. Событие бывает created (объект вошёл в множество) и updated (объект множества изменился); потоки читаются независимо, каждый своим курсором. Поле event описывает вашу историю, а не историю объекта: изменившийся тендер, которого вы раньше не получали, приедет как created.
Курсор — непрозрачная подписанная строка cur_<payload>.<sig>: позиция в конкретной ленте для конкретного набора событий. Возвращайте её как есть. Курсор идёт по оси индексации (когда объект обнаружен нами), а не по датам публикации, поэтому «отставшая» закупка не проваливается за курсор, а доезжает следующим запросом. Хронологию сортируйте у себя.
Версия — счётчик изменений объекта. Пара id + version — ключ дедупликации на вашей стороне.
Первый запрос отдаёт снапшот (всё, что подходит под фильтр сейчас), дальше идёт живая лента (всё, что появится позже). Граница фиксируется один раз — объекты не теряются и не приходят дважды.
Состояния ответа
state | HTTP | Что делать |
|---|---|---|
ready | 200 | Обычный ответ, лента собрана. |
partial | 200 | Данные читать можно, снапшот ещё дособирается; total может отсутствовать. Продолжайте курсором. |
preparing | 202 | Читать нечего, идёт сбор. Повторите тот же запрос через retry_after секунд. |
Под пиковой нагрузкой API не отказывает, а отвечает 202 preparing; 429 означает только ваш лимит, но не перегрузку сервиса.
Конверт ответа QUERY
{
"filter_id": "flt_8a3c9d12",
"state": "ready",
"applied": { "events": ["created"], "status": ["active"] },
"results": [
{ "event": "created", "version": 1, "billed": true,
"tender": { "id": 4821337, "etp_id": "0173…" } }
],
"next_cursor": "cur_eyJ…",
"total": 5000,
"billed": { "created": 50, "updated": 0, "free": 0,
"period_total": 50, "balance_exhausted": false },
"expires_at": "2026-08-12T09:00:00Z"
}
Коллекции (/lists, /filters, /balance/ledger) отвечают другим конвертом: { "data": [...], "has_more": false, "next_cursor": null }, продолжение — параметром starting_after.
Биллинг
Оплата пообъектная, лицензий и суточных лимитов нет. Балансы раздельные: tenders и contracts.
- Тарифицируется первая доставка версии объекта, а не запрос: пустые ответы, поллинг и ретраи той же страницы бесплатны.
- Платны оба события —
createdиupdated(каждая новая версия). - Оплата постранично: нашли 5000, забрали две страницы по 50 — заплатили за 100. Глубину пагинации контролирует клиент.
- Дедуп между фильтрами: объект под двумя вашими фильтрами тарифицируется один раз.
GET /tenders/{id}иGET /contracts/{id}списывают, только если эта версия вам ещё не доставлялась.- При нехватке баланса страница отдаётся частично:
billed.balance_exhausted: true,next_cursorуказывает перед первым неоплаченным объектом, HTTP 200. Если доставить не удалось ничего —402 balance_exhausted. После пополнения чтение продолжается с того же места без потерь и дублей. - Каждое списание объяснимо построчно:
GET /balance/ledger(объект, версия, фильтр,request_id).
Типовые сценарии
1. Разовый поиск закупок
POST /tenders/query с критериями → читайте results, при необходимости следующая страница по next_cursor. Ограничивайте выдачу критериями, а не пагинацией: каждая страница платная.
2. Мониторинг новых закупок (основной сценарий)
- Первый запрос с критериями — получаете снапшот и
next_cursor. - Сохраняете только
next_cursor. - По расписанию (например, раз в 5–15 минут) шлёте
{"cursor": "<сохранённый>"}. - Пустой
results— нормально и бесплатно;next_cursorобновляете всегда. 409 cursor_expired— не ошибка: в теле уже лежит свежий срез и новый курсор, продолжайте с него.
3. Отслеживание изменений
То же самое, но при первом запросе "events": ["updated"]. Потоки created и updated независимы — держите по курсору на каждый.
4. Контракты поставщика или заказчика по ИНН
- Контракты конкретного поставщика:
POST /contracts/queryс{"supplier": {"inn": ["…"]}}. - Контракты заказчика: те же критерии закупок —
{"customer": {"inn": ["…"]}}. - Первое обращение к контрактной проекции может вернуть
202 preparing— повторите.
5. Свой список объектов
POST /lists (нужен Idempotency-Key) → POST /lists/{list_id}/items → дальше POST /tenders/query с {"list_id": "…"}: над списком работает такая же лента, как над критериями.
Рабочие примеры
Поиск (curl)
curl -X POST https://public.fraim.ru/api/v2/tenders/query \
-H "Authorization: Bearer $FRAIM_TOKEN" \
-H "Content-Type: application/json" \
-d '{
"keywords": ["ремонт кровли", "асфальт"],
"exception_keywords": ["ямочный"],
"regions": [77],
"okpd2": ["42.11"],
"price": {"from": 100000, "to": 5000000},
"status": ["active"],
"limit": 50
}'
Продолжение ленты (curl)
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": 50}'
Цикл мониторинга (Python)
import os, time, requests
API = "https://public.fraim.ru/api/v2"
H = {"Authorization": f"Bearer {os.environ['FRAIM_TOKEN']}"}
def poll(cursor):
"""Один шаг ленты. Возвращает (события, новый курсор)."""
body = {"cursor": cursor} if cursor else {
"keywords": ["асфальт"], "regions": [77], "status": ["active"], "limit": 100,
}
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))
return [], cursor
if r.status_code == 429: # свой лимит частоты
time.sleep(int(r.headers.get("Retry-After", 60)))
return [], cursor
if r.status_code == 409: # лента пересобрана: срез уже в теле
data = r.json()
return data["results"], data["next_cursor"]
r.raise_for_status()
data = r.json()
if data["billed"]["balance_exhausted"]:
raise SystemExit("баланс исчерпан — пополните и продолжите с того же курсора")
return data["results"], data["next_cursor"]
cursor = None
while True:
events, cursor = poll(cursor) # cursor сохраняйте в свою БД
for e in events:
upsert(e["tender"], version=e["version"]) # дедуп по (id, version)
time.sleep(300)
Контракты поставщика по ИНН (Node.js)
const res = await fetch("https://public.fraim.ru/api/v2/contracts/query", {
method: "POST",
headers: {
Authorization: `Bearer ${process.env.FRAIM_TOKEN}`,
"Content-Type": "application/json",
},
body: JSON.stringify({ supplier: { inn: ["7701234567"] }, limit: 100 }),
})
if (res.status === 202) {
// проекция контрактов ещё собирается — повторить тот же запрос
}
const { results, next_cursor } = await res.json()
Создание списка (мутация с Idempotency-Key)
KEY=$(uuidgen) # один ключ на операцию, тот же при ретрае
curl -X POST https://public.fraim.ru/api/v2/lists \
-H "Authorization: Bearer $FRAIM_TOKEN" \
-H "Idempotency-Key: $KEY" \
-H "Content-Type: application/json" \
-d '{"name": "Проект Юг", "metadata": {"project_id": 42}}'
Справочник эндпоинтов
Всего маршрутов: 16. Сгенерировано из OpenAPI-схемы. Ответ 422 (ошибка валидации тела) возможен у любого маршрута с параметрами и ниже не повторяется.
Auth
| Метод и путь | Назначение |
|---|---|
GET /auth/tokens | Список API токенов |
POST /auth/tokens | Создать API токен (для API V2) |
DELETE /auth/tokens/{token_id} | Удалить API токен |
GET /auth/tokens — Список API токенов
Получить список всех API токенов пользователя
POST /auth/tokens — Создать API токен (для API V2)
Создаёт бессрочный API-токен — его и используют в своём коде. Требует авторизации: подойдёт сессия или уже имеющийся токен. Секрет возвращается один раз, повторно его не показать. Активных токенов у пользователя не больше 20: на следующем приходит 409 api_key_limit_reached, отзовите ненужный через DELETE /auth/tokens/{token_id}. Лимит частоты запросов считается на пользователя, а не на токен, — выпуск дополнительных токенов частоту не увеличивает.
| Параметр | Где | Тип | Обяз. | Описание и ограничения |
|---|---|---|---|---|
Idempotency-Key | header | string | нет | — |
Тело запроса — application/json, схема TokenCreateRequest:
| Поле | Тип | Обяз. | Описание и ограничения |
|---|---|---|---|
name | string | да | Название токена; длина 1–100 |
DELETE /auth/tokens/{token_id} — Удалить API токен
Деактивирует API токен. Удалять можно только свои токены; на чужой или несуществующий id — 404. token_id = 0 соответствует сессии из POST /auth/login: отзывать нечего, запрос возвращает успех и ничего не меняет.
| Параметр | Где | Тип | Обяз. | Описание и ограничения |
|---|---|---|---|---|
token_id | path | integer | да | — |
Idempotency-Key | header | string | нет | — |
Tenders
| Метод и путь | Назначение |
|---|---|
POST /tenders/query | Поиск и лента тендеров |
GET /tenders/{tender_id} | Карточка тендера |
POST /tenders/query — Поиск и лента тендеров
Единственный способ найти тендеры и следить за изменениями по ним. Ровно один способ задать множество: критерии поиска (keywords/regions/platforms/price/publish_date/customer_inn/status/ids) | filter_id | list_id — смешение способов даёт 400. Курсор из предыдущего ответа продолжает тот же поток событий (created/updated), заданный при первом запросе; events из тела игнорируется, если передан курсор.
Тело запроса — application/json, схема TenderQueryRequest:
| Поле | Тип | Обяз. | Описание и ограничения |
|---|---|---|---|
events | string[] | нет | значения: created | updated |
cursor | string | нет | — |
limit | integer | нет | диапазон 1–100; по умолчанию 100 |
wait | integer | нет | диапазон 0–25; по умолчанию 0 |
keywords | string[] | нет | Ключевые слова/фразы (ИЛИ: закупка подходит при совпадении любого элемента; многословный элемент ищется как фраза) |
exception_keywords | string[] | нет | Минус-слова: исключить закупки с этими словами |
regions | integer[] | нет | — |
platforms | integer[] | нет | — |
placement_types | integer[] | нет | — |
purchase_types | integer[] | нет | — |
okpd2 | string[] | нет | Коды ОКПД2 (допустимы префиксы, напр. '32.50') |
price | RangeFilter | нет | Начальная цена ЗАКУПКИ (НМЦК), не цена контракта |
publish_date | DateRangeFilter | нет | Дата публикации ЗАКУПКИ |
start_date | DateRangeFilter | нет | — |
end_date | DateRangeFilter | нет | — |
customer | CustomerFilter | нет | — |
status | string[] | нет | значения: active | completed | cancelled |
ids | integer[] | нет | — |
filter_id | string | нет | — |
list_id | string | нет | — |
GET /tenders/{tender_id} — Карточка тендера
Полная информация об одном тендере: описание, лоты, позиции (постранично: positions_page/positions_limit, с okpd2/ktru), документы. Структура объекта — как в старом API v2 (эталон). id_type=internal (по умолчанию) — внутренний id, id_type=external — id закупки на площадке (etp_id). Списание — только если версия объекта ещё не доставлялась (общая дедупликация с лентами QUERY /tenders).
| Параметр | Где | Тип | Обяз. | Описание и ограничения |
|---|---|---|---|---|
tender_id | path | string | да | — |
id_type | query | string | нет | по умолчанию "internal" |
positions_page | query | integer | нет | по умолчанию 1 |
positions_limit | query | integer | нет | по умолчанию 20 |
Contracts
| Метод и путь | Назначение |
|---|---|
POST /contracts/query | Контракты по множеству закупок |
GET /contracts/{contract_id} | Карточка контракта |
POST /contracts/query — Контракты по множеству закупок
Ровно один способ задать множество: критерии закупок (те же поля, что у QUERY /tenders, без ids) | filter_id | list_id | tender_ids (разово) | supplier (подписка на контракты поставщика по ИНН/названию — у такого фильтра нет тендерной проекции). Поиска по остальным полям контракта не существует как операции. Критерии закупок дают тот же filter_id, что и QUERY /tenders с теми же параметрами. supplier вместе с другим способом — пост-фильтр выдачи по поставщику (в filter_id не входит, повторяйте в каждом запросе). Первое обращение может вернуть preparing, пока не отработает contracts poller.
Тело запроса — application/json, схема ContractQueryRequest:
| Поле | Тип | Обяз. | Описание и ограничения |
|---|---|---|---|
events | string[] | нет | значения: created | updated |
cursor | string | нет | — |
limit | integer | нет | диапазон 1–100; по умолчанию 100 |
wait | integer | нет | диапазон 0–25; по умолчанию 0 |
keywords | string[] | нет | Ключевые слова/фразы (ИЛИ: закупка подходит при совпадении любого элемента; многословный элемент ищется как фраза) |
exception_keywords | string[] | нет | Минус-слова: исключить закупки с этими словами |
regions | integer[] | нет | — |
platforms | integer[] | нет | — |
placement_types | integer[] | нет | — |
purchase_types | integer[] | нет | — |
okpd2 | string[] | нет | Коды ОКПД2 (допустимы префиксы, напр. '32.50') |
price | RangeFilter | нет | Начальная цена ЗАКУПКИ (НМЦК), не цена контракта |
publish_date | DateRangeFilter | нет | Дата публикации ЗАКУПКИ |
start_date | DateRangeFilter | нет | — |
end_date | DateRangeFilter | нет | — |
customer | CustomerFilter | нет | — |
filter_id | string | нет | — |
list_id | string | нет | — |
tender_ids | integer[] | нет | — |
supplier | SupplierFilter | нет | — |
GET /contracts/{contract_id} — Карточка контракта
Полная информация об одном контракте, включая позиции (курсор positions_page/positions_limit), поставщиков, исполнение. id_type=internal (по умолчанию) — внутренний id, id_type=reestr — реестровый номер. Списание — только если версия ещё не доставлялась (общая дедупликация с лентой QUERY /contracts).
| Параметр | Где | Тип | Обяз. | Описание и ограничения |
|---|---|---|---|---|
contract_id | path | string | да | — |
id_type | query | string | нет | по умолчанию "internal" |
positions_page | query | integer | нет | по умолчанию 1 |
positions_limit | query | integer | нет | по умолчанию 20 |
Lists
| Метод и путь | Назначение |
|---|---|
GET /lists | Мои списки |
POST /lists | Создать список |
GET /lists/{list_id} | Детали и состав списка |
POST /lists/{list_id}/items | Добавить тендеры в список |
DELETE /lists/{list_id}/items | Убрать тендеры из списка |
GET /lists — Мои списки
Курсорная пагинация по спискам пользователя.
| Параметр | Где | Тип | Обяз. | Описание и ограничения |
|---|---|---|---|---|
limit | query | integer | нет | по умолчанию 20 |
starting_after | query | string | нет | — |
POST /lists — Создать список
Персональная изменяемая коллекция тендеров без TTL — живёт до явного удаления.
| Параметр | Где | Тип | Обяз. | Описание и ограничения |
|---|---|---|---|---|
Idempotency-Key | header | string | нет | — |
Тело запроса — application/json, схема ListCreateRequest:
| Поле | Тип | Обяз. | Описание и ограничения |
|---|---|---|---|
name | string | да | длина 1–255 |
description | string | нет | длина 0–2000 |
metadata | object | нет | — |
GET /lists/{list_id} — Детали и состав списка
Метаданные списка + состав (tender_id, курсорная пагинация).
| Параметр | Где | Тип | Обяз. | Описание и ограничения |
|---|---|---|---|---|
list_id | path | string | да | — |
limit | query | integer | нет | по умолчанию 100 |
starting_after | query | integer | нет | — |
POST /lists/{list_id}/items — Добавить тендеры в список
Идемпотентно — повторное добавление существующего id не создаёт дублей. Долив в тендерную ленту фильтра-над-списком синхронный: добавленный тендер сразу виден через QUERY /tenders/contracts {list_id}.
| Параметр | Где | Тип | Обяз. | Описание и ограничения |
|---|---|---|---|---|
list_id | path | string | да | — |
Idempotency-Key | header | string | нет | — |
Тело запроса — application/json, схема ListItemsRequest:
| Поле | Тип | Обяз. | Описание и ограничения |
|---|---|---|---|
ids | integer[] | да | элементов 1–10000 |
DELETE /lists/{list_id}/items — Убрать тендеры из списка
Идемпотентно. Уже доставленное остаётся доставленным (не отзывается); будущие события убранного тендера в ленты списка не попадают.
| Параметр | Где | Тип | Обяз. | Описание и ограничения |
|---|---|---|---|---|
list_id | path | string | да | — |
Idempotency-Key | header | string | нет | — |
Тело запроса — application/json, схема ListItemsRequest:
| Поле | Тип | Обяз. | Описание и ограничения |
|---|---|---|---|
ids | integer[] | да | элементов 1–10000 |
Filters
| Метод и путь | Назначение |
|---|---|
GET /filters | Мои живые подписки |
DELETE /filters/{filter_id} | Погасить подписку досрочно |
GET /filters — Мои живые подписки
Отладочная обвязка, не happy path — фильтры создаются только неявно любым QUERY.
| Параметр | Где | Тип | Обяз. | Описание и ограничения |
|---|---|---|---|---|
limit | query | integer | нет | по умолчанию 20 |
starting_after | query | string | нет | — |
DELETE /filters/{filter_id} — Погасить подписку досрочно
Сам фильтр (общий кеш параметров) не удаляется — гасится только ваша персональная подписка на него.
| Параметр | Где | Тип | Обяз. | Описание и ограничения |
|---|---|---|---|---|
filter_id | path | string | да | — |
Idempotency-Key | header | string | нет | — |
Balance
| Метод и путь | Назначение |
|---|---|
GET /balance | Баланс и расход за сегодня |
GET /balance/ledger | Журнал списаний |
GET /balance — Баланс и расход за сегодня
Баланс по типам (tenders/contracts) и количество тарифицированных доставок за текущие сутки (справочно, НЕ лимит).
GET /balance/ledger — Журнал списаний
Каждая строка — одна тарифицированная доставка: объект, версия, фильтр, request_id. Курсорная пагинация.
| Параметр | Где | Тип | Обяз. | Описание и ограничения |
|---|---|---|---|---|
limit | query | integer | нет | по умолчанию 100 |
starting_after | query | integer | нет | — |
Схемы объектов
Вложенные объекты фильтров и модели ответов, на которые ссылается справочник выше. Тела ответов QUERY-эндпоинтов схемой не описаны — их структура показана в разделе «Конверт ответа QUERY».
CustomerFilter
Заказчик/организатор ЗАКУПКИ (единый стиль с supplier — вложенный объект). inn — список (мониторинг нескольких заказчиков — обычный кейс). extra=forbid: незнакомое поле (напр. kpp) — явный 422, не молчаливое игнорирование.
| Поле | Тип | Обяз. | Описание и ограничения |
|---|---|---|---|
inn | string[] | нет | — |
ogrn | string | нет | длина 13–15 |
name | string | нет | длина 2–300 |
DateRangeFilter
| Поле | Тип | Обяз. | Описание и ограничения |
|---|---|---|---|
from | string | нет | — |
to | string | нет | — |
RangeFilter
| Поле | Тип | Обяз. | Описание и ограничения |
|---|---|---|---|
from | number | нет | — |
to | number | нет | — |
SupplierFilter
Поставщик контракта (единый стиль с customer — вложенный объект, inn — список, name — подстрока названия). Поставщик существует только в контракте — на множество ЗАКУПОК не влияет и в filter_id обычных фильтров не входит (ТЗ §6); сам по себе — supplier-режим (§6.1). ogrn (в отличие от customer) отсутствует: у поставщиков в БД нет ОГРН — матчить не по чему (резолв через справочник организаций покрыл бы ~2% поставщиков). kpp удалён 20.07.2026 ради симметрии с customer. Требуется хотя бы одно из inn/name (иначе 400).
| Поле | Тип | Обяз. | Описание и ограничения |
|---|---|---|---|
inn | string[] | нет | Список ИНН поставщиков (10 или 12 цифр каждый) |
name | string | нет | длина 2–300 |
TokenDeleteResponse
Ответ при удалении токена
| Поле | Тип | Обяз. | Описание и ограничения |
|---|---|---|---|
success | boolean | да | — |
message | string | да | — |
TokenInfo
Информация о токене с маскированным превью
| Поле | Тип | Обяз. | Описание и ограничения |
|---|---|---|---|
id | integer | да | — |
name | string | да | — |
token_preview | string | нет | Маскированный токен (например: abc...xyz) |
is_active | boolean | да | — |
created_at | string | да | — |
last_used_at | string | да | — |
TokenListResponse
Список токенов
| Поле | Тип | Обяз. | Описание и ограничения |
|---|---|---|---|
tokens | TokenInfo[] | да | — |
total | integer | да | — |
TokenResponse
Ответ с секретом — и при создании токена, и при входе (сессия)
| Поле | Тип | Обяз. | Описание и ограничения |
|---|---|---|---|
id | integer | да | ID токена; для сессии из POST /auth/login всегда 0 и ничего не идентифицирует |
name | string | да | — |
token | string | да | Секрет, показывается ОДИН раз. Для POST /auth/tokens — бессрочный API-ключ fraim_live_…/fraim_test_…; для POST /auth/login — сессия со сроком жизни 30 дней |
created_at | string | да | — |
Коды ошибок
Тело ошибки: {"error": {"code": "…", "message": "…", "request_id": "…"}}. request_id есть в каждом ответе и в журнале списаний — указывайте его в обращениях в поддержку.
| HTTP | code | Когда |
|---|---|---|
| 400 | validation_error | Невалидные параметры; ids>1000; >1 способа задать множество |
| 400 | invalid_cursor | Подпись курсора не сошлась |
| 400 | idempotency_key_required | Мутирующий запрос без заголовка Idempotency-Key |
| 401 | unauthorized | Нет или невалидный токен |
| 401 | invalid_session | Сессия кабинета истекла (30 дней) — войдите заново; к API-токенам не относится |
| 402 | balance_exhausted | Баланс 0 и ни один объект не доставлен |
| 404 | not_found | Объект/фильтр/список не существует или чужой список |
| 409 | cursor_expired | Лента пересобрана; в теле — свежий срез и новый курсор |
| 409 | api_key_limit_reached | Уже 20 активных токенов — отзовите ненужный и повторите |
| 422 | — | Стандарт валидации (в т.ч. превышение лимита metadata) |
| 429 | rate_limit_exceeded | Превышен лимит — запросов (100/мин на аккаунт), попыток входа или выпуска токенов. Подождите Retry-After и повторите |
| 202 | preparing | Не ошибка: идёт фоновый сбор, повторите тот же запрос |
Лимиты
| Что | Сколько | Подробности |
|---|---|---|
| Запросы к API | 100 в минуту | Считается на аккаунт, а не на токен. Сверх — 429 с заголовком Retry-After. |
| Объектов в сутки | без лимита | Расход ограничен только балансом. |
| Активных токенов | до 20 | На 21-м — 409 api_key_limit_reached: отзовите ненужный. |
| Выпуск токенов | 200 в час | Повтор с тем же Idempotency-Key за выпуск не считается. |
| Размер страницы | до 100 | Параметр limit. По умолчанию 100 в лентах (QUERY) и 20 в списках подписок и своих списков. |
| ID в запросе | до 10 000 | За раз — в POST /lists и запросах по списку. |
| Новых фильтров | до 50 | Считаются только неподтверждённые: фильтр, который вы продолжаете читать, из счёта уходит. |
| Попытки ввода кода | 5 на код | После пяти неверных вводов код перестаёт действовать — запросите новый через POST /auth/request-code. К API-токенам не относится. |
| Запросы кода | 5 в час | На один email. Код живёт 10 минут; новый запрос гасит предыдущий код. |
Частые ошибки агентов
| Так делать не надо | Почему и как правильно |
|---|---|
GET /tenders?keywords=… | Такого маршрута нет. Поиск — POST /tenders/query с JSON-телом. |
{"filter_id": "…", "keywords": […]} | Два способа задать множество сразу → 400. Оставьте один. |
{"page": 2}, {"offset": 100} | Пагинации по номеру страницы нет. Только cursor из next_cursor. |
Ретрай 202 preparing с изменённым телом | Другое тело — другой фильтр, сбор начнётся заново. Повторяйте байт-в-байт тот же запрос. |
| Прогон всей выдачи ради подсчёта | Каждая страница списывает баланс. Общее число — в поле total, считать пагинацией не нужно. |
Новый Idempotency-Key на каждом ретрае | Тогда повтор создаст второй объект. Ключ генерируется один раз на логическую операцию. |
supplier в POST /tenders/query | Поставщик существует только у контракта. По поставщику ищите в POST /contracts/query. |
status в POST /contracts/query | Игнорируется: контракты бывают только у завершённых закупок. |
Хранение filter_id и параметров ради продолжения | Достаточно последнего next_cursor — он самодостаточен. |
| Токен в браузерном коде или в репозитории | Токен = доступ к балансу. Только серверное окружение, только переменная окружения. |
Ссылки
- Этот документ в Markdown: https://api.fraim.ru/ai.md
- Карта документации для ИИ: https://api.fraim.ru/llms.txt
- OpenAPI 3.1: https://public.fraim.ru/api/v2/openapi.json
- Документация для людей: https://api.fraim.ru/docs
- Регистрация и токены: https://lk.api.fraim.ru