Best API

Десятки поставщиков описывают один товар по-разному — мы приводим их прайсы к единому канону, чтобы вы получали один чистый каталог, а не зоопарк написаний. Публичный REST API: бренды, коллекции, товары и типизированные характеристики. Авторизация по ключу, cursor-пагинация, ошибки в формате RFC 9457.

Песочница — без регистрации

Запросы уходят к тем же ручкам, что обслуживают дилеров: те же ответы, картинки вариантами, фасеты со счётчиками. Ключ не нужен — его подставляет наш сервер, в браузер он не попадает.

Песочница на этом стенде пока не настроена — запросы вернут ошибку.

Запрос

Счётчики каталога (товары, бренды, коллекции, разделы, поставщики), момент последнего обновления и крупнейшие бренды/коллекции по именам. Товаров не отдаёт — показывает масштаб.

Тот же вызов со своим ключом
curl -H "Authorization: Bearer ВАШ_КЛЮЧ" "http://localhost/api/public/v1/demo/stats"

Качество данных

Боль номер один рынка — десятки поставщиков описывают один и тот же товар по-разному: регистр, сокращения, порядок слов, формат размера. Руками это не свести (два-три прайса ещё терпимо, десятки — нет), а агентства и штатные интеграторы за эту работу обычно не берутся. Наш слой нормализации делает это на каждом импорте — ниже иллюстративный пример того, во что схлопываются такие написания.

Пришло от 3 поставщиков
  • Керамогранит ITALON Genesis Bianco Perla 60x60 полированный
  • Плитка керамогран. ITALON GENESIS BIANCO PERLA 600х600 полир.
  • Керамогранит Italon Genesis Bianco Perla пол. 60х60см, 1 сорт
Одна каноническая карточкаКерамогранит Italon Genesis Bianco Perla, 60×60 см, полированный

Дальше — тот же принцип на характеристиках и остатках: цена и наличие видны по каждому поставщику отдельно (offers[] ), а агрегаты на карточке (price_min , offers_count , suppliers_count) считаются из них же, а не берутся с последнего импорта поверх предыдущих.

Базовый URL

/api/public/v1

Авторизация — заголовок Authorization: Bearer <ключ>.

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

curl -H "Authorization: Bearer <ваш-ключ>" \
  "/api/public/v1/products?limit=2"

Ответ:

{
  "data": [
    {
      "public_id": "clx0pub0001",
      "slug": "cifre-alchimia-decor-9x9",
      "name": "Cifre Alchimia Decor 9x9",
      "price_from": 1490,
      "in_stock": true,
      "offers_count": 3,
      "suppliers_count": 2
    },
    {
      "public_id": "clx0pub0002",
      "slug": "italon-genesis-bianco-perla-60x60",
      "name": "Italon Genesis Bianco Perla 60x60",
      "price_from": 2140,
      "in_stock": true,
      "offers_count": 5,
      "suppliers_count": 3
    }
  ],
  "page": { "next_cursor": "Y2x4MGJyYW5kMDAwMQ", "has_more": true }
}

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

TS-клиент (SDK)

Не хотите вручную собирать запросы — в репозитории есть типобезопасный dependency-free fetch-клиент @best-api/public-api-sdk (пакет packages/sdk): типы ручек генерируются из этого же OpenAPI-контракта и всегда синхронны с API, курсорная пагинация спрятана в асинхронный генератор, ошибки — единый PublicApiError с машинным кодом вместо разбора текста.

Пакет скоро появится в npm — до публикации запросите dist-архив у нас напрямую.

import { createPublicApiClient } from '@best-api/public-api-sdk';

const client = createPublicApiClient({
  baseUrl: 'https://<ваш-домен>/api/public/v1',
  apiKey: process.env.API_KEY!,
});

const { data } = await client.listProducts({ limit: 50 });
for await (const product of client.iterateProducts({ limit: 100 })) {
  // upsert product в свой каталог…
}

// дифф-лента: new / updated / removed одним проходом, since двигать по as_of
for await (const page of client.iterateChanges({ since: lastSyncIso })) {
  // page.new / page.updated / page.removed…
}

Ручки

  • GET/api/public/v1/profileДанные клиента и его доступы
  • GET/api/public/v1/policyДействующая политика контракта
  • GET/api/public/v1/propertiesХарактеристики (типизированный реестр)
  • GET/api/public/v1/countriesСправочник стран (ISO 3166-1)
  • GET/api/public/v1/measuresСправочник единиц измерения (ОКЕИ)
  • GET/api/public/v1/productsТовары (фильтры updated_after, created_after, id; сорт new/цена)
  • GET/api/public/v1/products/exportBulk-export товаров (NDJSON)
  • GET/api/public/v1/products/facetsДоступные значения фильтров и счётчики под текущий выбор — для сайдбара
  • GET/api/public/v1/products/:idOrSlugКарточка товара
  • GET/api/public/v1/products/:id/offersПредложения поставщиков по товару
  • GET/api/public/v1/product-idsПолный список public_id товаров вертикали
  • GET/api/public/v1/brandsБренды каталога
  • GET/api/public/v1/brands/facetsФасеты справочника брендов
  • GET/api/public/v1/brands/categories/:slugКанонический slug категории брендов
  • GET/api/public/v1/brands/:idOrSlugКарточка бренда
  • GET/api/public/v1/collectionsКоллекции каталога
  • GET/api/public/v1/collections/facetsФасеты режима витрины «Коллекции»
  • GET/api/public/v1/collections/:idOrSlugКарточка коллекции
  • GET/api/public/v1/collections/:brandSlug/:slug/resolveКанонический slug коллекции (brand-scoped)
  • GET/api/public/v1/storesСклады и магазины каталога
  • GET/api/public/v1/store-itemsОстатки товаров по складам
  • GET/api/public/v1/changesДиф-лента: что изменилось в срезе клиента с момента since
  • GET/api/public/v1/webhooksМои подписки на вебхуки
  • POST/api/public/v1/webhooksЗарегистрировать приёмник — секрет подписи приходит в ответе один раз
  • PATCH/api/public/v1/webhooks/:idИзменить подписку (адрес, события, пауза)
  • DELETE/api/public/v1/webhooks/:idУдалить подписку
  • POST/api/public/v1/webhooks/:id/rotate-secretСменить секрет подписи
  • POST/api/public/v1/webhooks/:id/pingТестовая доставка на приёмник
  • GET/api/public/v1/webhooks/:id/deliveriesПоследние доставки подписки
  • GET/api/public/v1/menuМеню каталога: разделы вместе с подкатегориями-лендингами
  • GET/api/public/v1/categoriesДерево разделов каталога
  • GET/api/public/v1/categories/:slug/resolveКанонический slug раздела
  • GET/api/public/v1/categories/:categorySlug/facet-groupsФасетные группы раздела (состав панели фильтров)
  • GET/api/public/v1/categories/:categorySlug/presetsПресеты фасетов раздела (подкатегории-лендинги)
  • GET/api/public/v1/categories/:categorySlug/presets/:slug/resolveКанонический slug пресета раздела

Всё, кроме вебхуков, — только чтение. Ручки /webhooks меняют состояние: тело — JSON, нужен scope webhooks и ключ дилера. Контракт доставки — в секции «Вебхуки».

Вебхуки

Push вместо опроса по расписанию: как только у поставщика меняются цены или остатки, мы сами шлём POST на ваш адрес. Тело события — окно изменений со ссылкой на /changes: сами позиции забираете этой ручкой, поэтому пропущенная доставка не означает потерянных данных.

Приёмник регистрирует ваша интеграция своим же ключом — POST /api/public/v1/webhooks (нужен scope webhooks), события price_changed и stock_changed. Секрет подписи приходит в ответе один раз.

Требования к приёмнику

  • Только https и только публичный адрес. Приёмник во внутренней сети (loopback, приватные диапазоны, адреса метаданных облака) не регистрируется, а имя, резолвящееся туда же, отбивается в момент соединения.
  • Принято — это ответ 2xx. Тело ответа мы не читаем и не храним: нужен только факт приёма.
  • Редиректы мы НЕ следуем: любой 3xx (301, 302, 307, 308) считается отказом приёмника и уходит в ретраи. Адрес подписки должен быть конечным — не тем, который переадресует на «правильный» URL.
  • Ответ должен уложиться в 10 секунд, иначе попытка считается неудачной.
  • Неудачная попытка повторяется: до 6 попыток с растущей паузой — минута, 5 минут, 15 минут, час, 6 часов. Доставка идёт фоновой задачей, порядок событий не гарантируется.

Подпись и заголовки

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

X-Webhook-Signature: sha256=HMAC-SHA256(секрет, "<timestamp>.<тело>")
X-Webhook-Event:     price_changed | stock_changed | ping
X-Webhook-Delivery:  идентификатор доставки (одинаков у всех попыток)
X-Webhook-Timestamp: время отправки, оно же входит в подпись
X-Webhook-Attempt:   номер попытки, начиная с 1

Ротация секрета не обрывает старый секрет мгновенно: 24 часа после ротации доставка подписана ОБЕИМИ подписями сразу — новым секретом и ещё не истёкшим старым, через запятую в X-Webhook-Signature (совпадения любой из них достаточно). За это время переставьте секрет на приёмнике; по истечении окна старый секрет перестаёт приниматься.

Здоровье приёмников, история доставок, тестовая доставка ( ping ) и ротация секрета — в личном кабинете .

Версии и совместимость

Версия — сегмент пути: /api/public/v1. Пока адрес начинается с /public/v1, контракт по этому адресу не ломается: ломающее изменение выходит новым сегментом версии (/public/v2), а не правкой текущей.

Меняем внутри версии без предупреждения

  • новые ручки и новые необязательные параметры запроса
  • новые поля в ответе
  • новые значения перечислений (kind, статусы, коды ошибок)
  • изменение порядка ключей в JSON и текстов описаний

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

Считаем ломающим (только через новую версию)

  • удаление или переименование ручки, поля ответа, параметра
  • смена типа или формата поля (строка → число, другой формат даты)
  • новый обязательный параметр запроса или ужесточение валидации
  • смена смысла значения при том же имени поля
  • смена структуры ошибки или HTTP-статуса штатного сценария

Как узнаете о депрекации

Из ответа, а не из письма. Устаревшая ручка (или отдельное поле её ответа) продолжает работать как обычно, но каждый её ответ несёт заголовки Deprecation (RFC 9745) и Sunset (RFC 8594) с датой отключения, а Link ведёт на эту политику и на замену:

Deprecation: @1788220800
Sunset: Thu, 01 Apr 2027 00:00:00 GMT
Link: <https://<домен>/developers#api-policy>; rel="deprecation"; type="text/html",
      </api/public/v2/products>; rel="successor-version"

То же самое — в OpenAPI-спеке: операция помечена deprecated, детали (что именно устарело, до какой даты и чем заменено) — в расширении x-deprecation. Актуальный список депрекаций всегда виден в спеке, отдельной рассылки для этого не нужно.

Между объявлением и отключением — срок, заданный политикой контракта. Срок проверяется кодом: объявление с более коротким сроком не проходит сборку API, поэтому «выключили завтра» технически невозможно. Машинное значение — в ответе GET /policy (поле deprecation.min_notice_days).

Лимиты и SLA

Лимиты и перегрузка

  • Фактический лимит ключа — в каждом ответе: RateLimit-Limit , RateLimit-Remaining , RateLimit-Reset. Считайте по ним, а не по цифрам на этой странице.
  • Те же цифры машиночитаемо — GET /policy: лимит вашего ключа, потолок страницы, окна кэша и срок предупреждения о депрекации.
  • Превышение — 429 с Retry-After в секундах. Корректный клиент ждёт указанное время, а не повторяет запрос сразу.
  • Потолок страницы и состав полей зависят от поверхности ключа: витринный отдаёт меньше позиций за раз и без оптовых агрегатов.

Свежесть данных

  • Каталог обновляется по факту импорта прайсов поставщиков, а не по расписанию ответа: заголовок X-Dataset-Version показывает, на каком срезе данных получен ответ.
  • Сколько ответ разрешено переиспользовать в клиенте и в общем кэше, говорит Cache-Control каждого ответа: в пределах этого окна ответ может отставать от базы.

Что гарантируем

  • Совместимость внутри версии и минимум 180 дней между объявлением депрекации и отключением — см. раздел выше.
  • Ошибки в одном формате (RFC 9457): машинный код и человеческое пояснение, без разбора текста.
  • ETag / If-None-Match на читающих ручках: повторный запрос без изменений отвечает 304 без тела.
  • Инкрементальная синхронизация без полной перевыкачки: /changes и updated_after.
  • Текущая доступность видна публично и обновляется автоматически — страница статуса.
  • Заблаговременное объявление плановых работ баннером на той же странице статуса — отдельно от фактического здоровья.

Чего не гарантируем на текущем этапе

  • Числового SLA доступности (99,9 % и подобное) с компенсациями и кредитами сейчас нет — сервис работает на одной инсталляции, без резервирования и мультирегиона.
  • Круглосуточного дежурства нет: на инциденты реагируем в рабочие часы (Москва).
  • Нулевых окон обслуживания: короткие перерывы на обновление возможны — ретрай на стороне клиента всё равно обязателен.
  • Восстановление после аварии — из суточного дампа (снимается и проверяется восстановлением ежедневно), то есть в худшем случае теряются изменения каталога за срок до суток.

Текущее состояние сервиса и время ответа API — страница статуса . Нужны договорные гарантии доступности под вашу интеграцию — обсуждаем отдельно, до подписания их здесь не обещаем.