Public API cookbook

Документ для инженеров, которые впервые подключают Tracium Public API. Если лежит вопрос «что делать в моей ситуации» — ответ здесь. Контракт целиком описан в 20-architecture/schemas/api/public-api.openapi.yaml; этот cookbook — навигация поверх него.

Базовое окружение

ОкружениеURL
Productionhttps://api.tracium.ru/v1
Staginghttps://api.staging.tracium.ru/v1
Local devhttps://api.tracium.dev/v1

Везде, где в примерах ниже встречается BASE, подставь URL нужного окружения. API-token (trk_…) клиент получает в кабинете на странице API ключи. Все запросы отправляются с заголовком Authorization: Bearer trk_….

Что сейчас live, а что target

Документ описывает и live-сценарии, и целевую карту API. Для интеграции клиента начинать нужно только с live endpoint’ов:

СтатусEndpointЗачем нужен
LiveGET /v1/whoamiПроверить токен и identity клиента
LiveGET /v1/productsПролистать read-side каталог
LiveGET /v1/products/{id_or_viewable}Получить карточку товара, описание, характеристики, supplier offers
LivePOST /v1/products/resolveСопоставить supplier SKU с canonical product refs
LivePOST /v1/search/proposalsПо списку позиций получить предложения: поставщик, цена, наличие, доставка

Остальные endpoint’ы в OpenAPI (/offers/*, /pricing, /estimates/*, /enrichment-jobs/*, /products/analog-search) пока считаются target-контрактом. Их нельзя делать основным путём клиентской интеграции, пока handler не появится в live coverage.

Первый рабочий сценарий: список товаров → поставщики, цена, наличие, доставка

Это базовый сценарий для инженера с готовой таблицей позиций.

Входные данные

Минимальная строка входной таблицы:

Поле клиентаКак передавать в Tracium
Внутренний номер строки сметыitems[].id
Supplier SKUPOST /products/resolveitems[].canonical_ref
Tracium product ref, если уже известенсразу items[].product_ref в POST /search/proposals
Количествоitems[].qty_requested

product_ref для POST /search/proposals — это viewable_id вида TR-XXXXXXX или canonical UUID. Если у клиента на руках supplier SKU / артикул поставщика, сначала вызываем POST /products/resolve.

Шаг 1. Проверить токен

curl -sS "$BASE/whoami" \
  -H "Authorization: Bearer $TRACIUM_TOKEN"

Если ответ 200, можно выполнять рабочие запросы. Если 401, токен невалиден или отозван. Если 403, у токена не хватает scope.

Шаг 2. Сопоставить supplier SKU с canonical product

curl -sS "$BASE/products/resolve" \
  -H "Authorization: Bearer $TRACIUM_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{
    "items": [
      { "id": "line-1", "supplier": "etm", "supplier_sku": "ABC123", "qty_requested": 5 },
      { "id": "line-2", "supplier": "russvet", "supplier_sku": "DEF5678", "qty_requested": 12 }
    ]
  }'

Как читать результат:

ПолеЧто означает
items[].status=resolvedНайден один canonical product; можно идти в /search/proposals
items[].canonical_refUUID canonical product для items[].product_ref следующего запроса
items[].viewable_idЧеловекочитаемый ref TR-XXXXXXX, его тоже можно использовать как product_ref
items[].offer_refОффер, по которому exact selector был сопоставлен
items[].status=not_foundТакой supplier + supplier_sku не найден в read-side каталоге
items[].status=unsupported_queryShape запроса валиден, но selector ещё не live

Контракт уже принимает manufacturer, mpn, query, characteristics[] для будущего параметрического resolver’а. Сейчас такие строки возвращают unsupported_query с reason characteristic_resolver_not_live, чтобы клиент не спутал подготовленный контракт с работающим подбором.

Шаг 3. Получить предложения по списку

curl -sS "$BASE/search/proposals" \
  -H "Authorization: Bearer $TRACIUM_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{
    "mode": "reference",
    "target_currency": "RUB",
    "items": [
      { "id": "line-1", "product_ref": "01HF0000000000000000000001", "qty_requested": 5 },
      { "id": "line-2", "product_ref": "01HF0000000000000000000002", "qty_requested": 12 }
    ],
    "policy": {
      "trace": { "inline_level": "summary" }
    }
  }'

Что вернётся по каждой позиции:

Где смотретьЧто означает
items_outcome[]Собрались ли предложения по строке сметы
proposals_by_item[query_item_id]Индексы предложений этой строки в общем массиве proposals[]
proposals[].supplier_ref / seller_refПоставщик и продавец
proposals[].priceФинальная цена в target_currency; mode показывает точность цены
proposals[].stockСколько доступно и покрывает ли это qty_requested
proposals[].deliveryДиапазон срока доставки и статус доступности
proposals[].canonical_refCanonical product, по которому можно загрузить карточку товара
proposals[].offer_refКонкретный оффер, из которого собрано предложение
proposals[].positionПозиция в ранжировании; 0 — лучший вариант для строки

Алгоритм чтения ответа:

  1. Идём по items_outcome[].
  2. Если status=no_results, фиксируем строку как не найденную.
  3. Если status=ok, берём proposals_by_item[query_item_id].
  4. По каждому индексу читаем proposals[index].
  5. Для закупочного решения сравниваем position, price, stock.status, stock.available_qty, delivery.status, delivery.lead_time_*.

Шаг 4. Загрузить описания и характеристики

POST /v1/search/proposals отдаёт коммерческое предложение, но не является полной карточкой товара. Когда нужно показать описание, изображения и характеристики, берём уникальные canonical_ref из выбранных предложений и загружаем карточки:

curl -sS "$BASE/products/01HF0000000000000000000000" \
  -H "Authorization: Bearer $TRACIUM_TOKEN"

В Product важны поля:

ПолеЧто использовать
name, description, images[]Клиентское отображение товара
characteristics[]Характеристики для проверки соответствия смете
supplier_offers[]Read-side список офферов поставщиков по этому товару
prices, stock, deliveryТекущий primary snapshot; для выбора по смете приоритетнее proposals[]

Правило: для решения «у кого купить эту смету» основным источником остаётся POST /v1/search/proposals. GET /v1/products/{id_or_viewable} нужен как дозагрузка карточки и характеристик.

Карта сценариев

Что у тебя на рукахКуда идти
Supplier SKU и количестваПервый рабочий сценарий
Список Tracium product refs и количеств (смета)Сценарий A
Нужно пролистать доступные карточки каталогаСценарий B
Известен canonical_id или viewable_id, нужно описаниеСценарий C
Нужно подобрать по характеристикамPOST /products/resolve shape подготовлен, live-результат пока unsupported_query
Нужно найти аналоги по неточным характеристикамPOST /products/analog-search — target
Нужны все поставщики данного товараСценарий D
Нужна история цен или остатковСценарий E
Хочу пересчитать цену по правилам клиентаСценарий F
Чем cached отличается от liveРаздел про RefreshMode

Сценарий A — собрать смету по известным товарам

Задача. На входе — таблица «Tracium product ref + количество». Нужно понять, у каких поставщиков есть эти товары, по какой цене, в каком количестве и с каким сроком поставки.

Endpoint. POST /v1/search/proposals — главный сценарный endpoint.

Статус. Live. Это основной путь для первой клиентской интеграции.

Важно: в текущем live API items[].product_ref — это TR-XXXXXXX или canonical UUID. Supplier SKU как product_ref не поддержан.

curl -sS "$BASE/search/proposals" \
  -H "Authorization: Bearer $TRACIUM_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{
    "mode": "reference",
    "target_currency": "RUB",
    "items": [
      { "id": "p1", "product_ref": "TR-ABC1234", "qty_requested": 5  },
      { "id": "p2", "product_ref": "TR-DEF5678", "qty_requested": 12 },
      { "id": "p3", "product_ref": "TR-GHI9999", "qty_requested": 1  }
    ],
    "policy": { "trace": { "inline_level": "summary" } }
  }'

Что задаём:

  • mode: reference — не требуем строгой свежести; pipeline соберёт предложения и из «не свежих» наблюдений (рабочее значение по умолчанию для большинства смет).
  • target_currency: RUB — все цены вернутся в рублях; конвертация (если поставщик в евро или долларах) применится автоматически по курсу слоя 3a.
  • Каждый items[].id — твой стабильный идентификатор строки сметы; в ответе он попадёт в query_item_id и поможет сшить ответ обратно с таблицей.
  • policy.trace.inline_level: summary — pipeline вернёт безопасный trace-блок (полезен для отладки причин «почему вышло именно это предложение»).

Как читать ответ

{
  "search_id": "01HF…UUID",          // та же UUID есть в заголовке X-Search-Id
  "items_echo":      [ /* эхо твоих items[] */ ],
  "items_outcome":   [
    { "query_item_id": "p1", "proposal_count": 3, "status": "ok" },
    { "query_item_id": "p2", "proposal_count": 0, "status": "no_results" },
    { "query_item_id": "p3", "proposal_count": 1, "status": "ok" }
  ],
  "proposals_by_item": {
    "p1": [0, 1, 2],
    "p3": [3]
  },
  "proposals": [
    { "candidate_id": "…", "query_item_id": "p1", "supplier_ref": "etm",
      "price":   { "mode": "fixed",
                   "amount": { "value": "129050", "currency": "RUB", "precision": 2 } },
      "stock":   { "available_qty": 17, "status": "ok",
                   "source_mode": "cached_actual" },
      "delivery":{ "lead_time_min_hours": 24, "lead_time_max_hours": 48,
                   "status": "ok" },
      "score": 1.0, "position": 0 },
    /* … остальные предложения */
  ],
  "trace": { /* … включается через policy.trace.inline_level */ }
}

Алгоритм чтения:

  1. Прохожу по items_outcome[]. Если status=no_results — по этой строке сметы предложений нет, идём дальше.
  2. Для status=ok беру proposals_by_item[query_item_id] — массив индексов в proposals[]. Они уже отсортированы по убыванию релевантности (position=0 — лучшее).
  3. По индексу читаю само предложение: priceadjustments[] — разбивкой по НДС/наценкам/скидкам), stock, delivery, supplier_ref, seller_ref, lifecycle, source_mode.
  4. Если включил trace — trace.entries[] пошагово показывает, какие слои pipeline’а отбросили или модифицировали кандидатов.

Когда нужны характеристики/описания товаров

POST /v1/search/proposals не отдаёт описание товара (description, characteristics). После того как вернулись предложения, для каждого интересующего canonical_ref дернуть GET /v1/products/{canonical_ref} — там полные характеристики.


Сценарий B — пролистать каталог

Задача. Нужна страница доступных canonical-карточек для UI или ручной проверки состава каталога.

Endpoint. GET /v1/products?page=…&page_size=….

curl -sS -G "$BASE/products" \
  -H "Authorization: Bearer $TRACIUM_TOKEN" \
  --data-urlencode "page=1" \
  --data-urlencode "page_size=20"

Что вернётся: страница ProductListResponse с canonical-карточками, описанием, характеристиками и текущим read-side срезом по primary supplier snapshot: цена, остатки, доставка и список supplier_offers.

Если нужно выбрать поставщика под количество, валюту, availability и delivery, дальше берёшь подходящий id или viewable_id, кладёшь в items[] запроса POST /v1/search/proposals и получаешь ранжированные предложения.


Сценарий C — получить карточку известного товара

Задача. На руках есть canonical_id (UUID) или человекочитаемый viewable_id формата TR-XXXXXXX. Нужны описание и характеристики, без цен.

Endpoint. GET /v1/products/{id_or_viewable}.

curl -sS "$BASE/products/TR-ABC1234" \
  -H "Authorization: Bearer $TRACIUM_TOKEN"

Вернётся Product с полным набором: name, description, images[], characteristics[], lifecycle_status, а также текущий read-side срез prices, stock, delivery и supplier_offers. Для ранжированного выбора поставщика под конкретное количество всё равно используем /search/proposals.


Сценарий D — посмотреть офферы поставщиков

Задача. Знаем товар и хотим увидеть всех поставщиков, у которых он есть, без расчёта итоговой цены.

Endpoint. GET /v1/products/{id_or_viewable}/offers.

Статус. Target. Для live-интеграции сейчас используйте GET /v1/products/{id_or_viewable} и поле supplier_offers[].

curl -sS "$BASE/products/TR-ABC1234/offers?only_available=true&limit=50" \
  -H "Authorization: Bearer $TRACIUM_TOKEN"

Параметр only_available=true отрежет офферы с нулевым стоком. Возвращаются карточки оффера с последним наблюдением цены/наличия и ссылкой на поставщика. Цены тут «как есть», без применения правил клиента — для финальной цены идти в POST /v1/pricing или POST /v1/search/proposals.


Сценарий E — получить историю наблюдений

Задача. Аудит, графики, выявление трендов: нужны точки наблюдений по конкретному офферу за период.

Endpoint. GET /v1/offers/{id}/observations.

Статус. Target. В live coverage этот endpoint пока не зарегистрирован.

curl -sS -G "$BASE/offers/$OFFER_UUID/observations" \
  -H "Authorization: Bearer $TRACIUM_TOKEN" \
  --data-urlencode "from=2026-04-01T00:00:00Z" \
  --data-urlencode "to=2026-05-01T00:00:00Z" \
  --data-urlencode "kind=price"

kindprice, stock или all. Данные append-only: каждое наблюдение — отдельная запись с observed_at. Пагинация — через next_cursor.


Сценарий F — точечный расчёт цены

Задача. Известен offer_id или canonical_id одной позиции, нужна финальная цена клиента (со всеми правилами: НДС, наценка, скидка, custom-handler) — без полного pipeline’а сметы.

Endpoint. POST /v1/pricing.

Статус. Target. В live coverage этот endpoint пока не зарегистрирован. Для сметного сценария используйте POST /v1/search/proposals.

curl -sS "$BASE/pricing" \
  -H "Authorization: Bearer $TRACIUM_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{
    "items": [
      { "offer_id":     "01HF…UUID", "qty": 5  },
      { "canonical_id": "01HG…UUID", "qty": 1 }
    ],
    "target_currency": "RUB"
  }'

Если задан canonical_id, движок сам выберет подходящий observation (с учётом visibility и preferred-sellers). Поддерживает batch до многих позиций.

POST /v1/pricing уместен в калькуляторах, экспорте прайс-листа, проверке стоимости одной позиции. Для сметы — POST /v1/search/proposals, который сделает всё то же самое плюс наличие, доставку и ранжирование.


Режимы RefreshMode: cached_only vs live

POST /v1/search/proposals поддерживает два режима обращения к данным, управляются через policy.refresh_mode:

  • cached_only (по умолчанию для всех mode, кроме mode=live). Pipeline читает только локальные кэши. Всегда быстрый, не зависит от доступности поставщика, не расходует customer credentials. Подходит для основной массы запросов.
  • live. Pipeline для каждого кандидата пытается сделать live-вызов к поставщику через customer credential. Если живой вызов не удался (нет credential, исчерпан бюджет на cache-warm, ошибка коннектора), pipeline откатывается на cached-данные и явно проставляет причину в полях stock.live_attempt_reason / delivery.live_attempt_reason (live_used, live_unavailable_no_credential, live_budget_exhausted, live_connector_failed и т.д.). Клиент видит, что попытка была и почему она не удалась.

Когда брать live: «прямо сейчас нужны точные данные у конкретного поставщика» (закупка, оперативный заказ, проверка перед отправкой). В остальных случаях кэшированные данные дешевле и быстрее.

Заголовки потребления и лимитов

Каждый ответ Public API несёт служебные заголовки, которые помогают клиенту следить за лимитами:

ЗаголовокЧто внутри
X-RateLimit-{Limit,Remaining,Reset}-{minute,hour,day,week}Скользящие окна
X-Quota-Requests-{Limit,Remaining,Reset}Биллинговая квота на запросы (обычно месяц)
X-Quota-Data-Bytes-*То же по объёму отданных данных
X-Usage-Packages-Requests-RemainingОстаток в prepaid-пакетах
X-Plan-CodeКод активного плана
X-Request-WeightСколько единиц списано с квоты за запрос
X-Search-IdUUID запуска (только у POST /search/proposals)

Превышение короткого окна — 429 rate_limited + Retry-After. Превышение биллинговой квоты — 429 quota_exhausted. Endpoint, недоступный в плане, — 402 plan_required.

Дальнейшее чтение

  • Полный контракт: 20-architecture/schemas/api/public-api.openapi.yaml.
  • Аутентификация и API-ключи: 30-services/cabinet-keys/, ADR-0051 (20-architecture/adr/0051-public-api-auth-and-api-key-rbac.md).
  • Семантика свежести и no-proxy: ADR-0014.
  • Visibility: ADR-0012.
  • Multi-protocol API (gRPC, WS): ADR-0015.