API для организаций: выгрузка намерений
Организации загружают в «ХочуМогу» каталог товаров пакетно — по ссылке на файл или прямым вызовом API, без ручного ввода через мастер. Конвейер сам превращает позиции каталога в намерения «Могу продать». Оба способа уже работают и доступны в личном кабинете организации: блок «Импорт по ссылке» (поле URL, автосинхронизация, кнопка «Проверить сейчас») и «Подключение по API» (выпуск токена доступа).
1. Аутентификация
Прямой вызов авторизуется персональным токеном организации в заголовке Authorization: Bearer <org_token>. Токен выдаётся в личном кабинете организации (блок «Подключение по API» → «Выпустить ключ»), показывается один раз в момент выпуска и привязан к аккаунту — загруженные товары становятся намерениями от его имени. Для импорта «по ссылке» токен не нужен: фидом управляет сама организация под обычной сессией в личном кабинете.
2. Два способа загрузки
Оба принимают один и тот же формат файла (раздел 3 ниже).
- Прямой вызов (push). Один запрос — один прогон синхронизации:
POST /v1/org/intentions/importс телом-файлом. - По ссылке (pull). Организация один раз указывает URL файла в блоке «Импорт по ссылке» личного кабинета; сервис сам периодически его забирает и синхронизирует (подробности — раздел 6).
POST /v1/org/intentions/import?dry_run=1
Authorization: Bearer nmr_org_<32 hex-символа>
Content-Type: application/x-yaml
# тело — сам yaml-файл целиком, БЕЗ JSON-обёртки (см. формат ниже)
dry_run=1 — сухой прогон: тот же отчёт, что и боевой импорт, но без создания и изменения намерений (пригодится, чтобы проверить файл перед первой загрузкой). remove_missing=1 — закрыть намерения, чьи товары пропали из текущего файла (без параметра пропавшие товары остаются как есть).
3. Формат файла (YAML)
Файл — список товаров вашего каталога, а не готовых намерений: сервис сам решает, во что превратить каждый товар. Как поле обязателен только offers[].id — без него запись не свяжется с намерением, сервис вернёт по товару ошибку и не создаст черновик. category тоже нужна практически всегда: без известной сервису категории товар не пропадает с ошибкой всего запроса, но пропускается (раздел 5). Дальше — не поля, а требования к результату: импорт всегда создаёт намерения «Могу продать», а для этого типа по умолчанию обязательны цена и хотя бы один тег, который узнает словарь характеристик — оба разобраны в подсказках под примером.
shop: "Мой магазин"
city: "Хабаровск" # территория по умолчанию для всех товаров
offers:
- id: "sku-101" # ваш идентификатор — для синхронизации, обязателен
name: "Дрель-шуруповёрт Bosch GSR 12V-15, 2 АКБ, кейс"
category: "Инструменты / Электроинструмент / Дрели-шуруповёрты"
vendor: "Bosch"
price: 8990
params:
"Тип аккумулятора": "Li-Ion"
"Наличие реверса": "да"
"Цвет": "синий"
- id: "sku-102"
name: "Платье летнее хлопковое, синее, 44 размер"
category: "Женщинам / Женская одежда / Платья"
vendor: "Zarina"
price: 2490
city: "Владивосток" # переопределяет город магазина для этого товара
params:
"Размер": "44"
"Цвет": "синий"
- id — ваш идентификатор товара (он же
external_id), обязателен: без него сервис не создаст запись, а вернёт по этому товару ошибку «у товара нет id» вerrorsотчёта (раздел 5) — в намерение он не превратится. - category — путь по вашему дереву категорий через
/, из последнего уровня сервис определяет НАЗВАНИЕ будущего намерения по своему словарю (например, лист «Дрели-шуруповёрты» → название «дрель-шуруповёрт» — то, что покупатель наберёт в своём «хочу купить»). Пустая или незнакомая сервису категория — товар пропускается (попадает вskippedотчёта, раздел 5, с причиной «пустая категория» или «неизвестная категория: …»), а не превращается в намерение с произвольным названием; список известных категорий пополняем по обращениям. - name — обычное название товара из вашего каталога, необязательно. Если указано — идёт в описание намерения как есть (в заголовок НЕ становится — заголовок из категории выше); если не указано вовсе — описание остаётся пустым, это не ошибка.
- vendor и params (произвольные пары «характеристика: значение») — становятся тегами намерения по внутреннему словарю, но только при ТОЧНОМ (без учёта регистра) совпадении имени характеристики со словарём: например, «Наличие реверса: да» → тег «с реверсом», а вот просто «Реверс: да» словарь не узнает по имени и молча отбросит. У каждого узнанного тега есть вес важности (зависит от категории товара, домена и самой характеристики), и в намерение он попадает, только если вес не ниже порога отсечения — например, «Цвет» у инструментов намеренно снижен (для подбора совпадений по дрели цвет не важен) и почти всегда отбрасывается, а у одежды, наоборот, входит в вес. В примере выше поэтому «Наличие реверса: да» у первого товара становится тегом «с реверсом», а «Цвет: синий» у него же — отбрасывается по весу; у второго товара (одежда) «Цвет: синий» тегом становится. Тегов на намерение остаётся немного (алгоритм подбора совпадений по тегам точнее работает с коротким набором самых важных, а не длинным списком характеристик). Отброшенное — не ошибка: агрегированную статистику (сколько раз и по какой причине) показывает поле
metrics.drop_reasonsотчёта (раздел 5); привязки «какой именно товар и характеристика» в отчёте нет — это сводка по всему фиду. - city — необязателен, задаёт территорию на уровне товара или всего магазина; действует каскад «товар → магазин → город профиля организации → вся страна» (первый заполненный уровень побеждает).
- price — цена в рублях. Фактически ОБЯЗАТЕЛЬНА: импорт всегда создаёт намерения «Могу продать», а для этого типа по умолчанию нужна цена больше нуля (шаг «Стоимость» мастера обязателен, пока администратор сервиса явно не отключит его для этого типа в настройках конструктора мастера — по умолчанию включён). Импорт никогда не проставляет «цена договорная» автоматически — если
priceне указан или ≤ 0, сервис не создаст намерение и вернёт по товару ошибку «price: укажите стоимость или «договорная»» вerrorsотчёта (раздел 5).
Все загруженные товары становятся намерениями «Могу продать» — другие типы (аренда, услуги, вакансии) через этот канал сейчас недоступны. Количество — не отдельное поле оффера, а тоже характеристика в params: сервис ищет среди них ЛЮБУЮ с именем, содержащим «количество в упаковке» (регистр не важен), и её числовое значение становится количеством намерения — единица берётся из суффикса имени через запятую (например, «Количество в упаковке, шт» → 200 шт.; без суффикса единица останется не указана). Нет такой характеристики или её значение не число — количество принимается за 1 шт. Состояние товара при импорте всегда «новое» — это не настраивается.
4. Синхронизация и обновления
- id связывает запись файла с намерением в сервисе: повторная загрузка того же
idобновляет намерение на месте (без версий, как и везде в сервисе), а не создаёт дубль. - Товар, пропавший из очередного файла, сам по себе никак не меняется — намерение остаётся опубликованным. Чтобы такие товары закрывались автоматически, добавьте
remove_missing=1к запросу (push) или включите тумблер «Закрывать пропавшие из фида» (импорт по ссылке). - Загруженные намерения проходят те же проверки, что и созданные вручную: нормализация тегов, фильтр недопустимых слов, лимиты активных намерений — и публикуются сразу.
5. Ответ и коды
Отчёт о прогоне — один и тот же формат у прямого вызова и у «Проверить сейчас» (раздел 6), но они по-разному сообщают об отказе — это две разные вещи, их легко перепутать:
- Прямой вызов (
POST /v1/org/intentions/import) — обычный REST: успех —200с отчётом ниже, отказ — HTTP-код с текстом ошибки (коды — дальше в этом разделе). - «Проверить сейчас» (
POST /v1/org/feed/sync, раздел 6) отвечает200практически всегда, даже если сама синхронизация не удалась — из специально обрабатываемых исходов ответа всего один не 200, он описан в разделе 6. Смотрите полеokиfeed.last_messageв теле ответа, а не HTTP-код.
Успешный прогон (прямой вызов) отвечает 200:
{
"shop": "Мой магазин",
"dry_run": false,
"counters": {
"total": 2, "created": 1, "updated": 1,
"unchanged": 0, "skipped": 0, "closed": 0, "errors": 0
},
"skipped": [],
"errors": [],
"metrics": {
"total": 2, "converted": 2, "skipped": 0,
"category_hit_rate": 1, "avg_tags": 2.5,
"drop_reasons": { "вес 15 < порога 25": 1 }
}
}
skipped[] — товары с пустой или незнакомой категорией (раздел 3), у каждой записи id товара и причина. errors[] — остальные отказы по товару: нет id, нет цены, ни одна характеристика не дала тега, лимит намерений, недопустимое слово — тоже с id и текстом причины. metrics — сводка по ВСЕМУ фиду, не по отдельному товару: total/converted/skipped — всего офферов / распознано по категории / пропущено, category_hit_rate — доля распознанных категорий (converted/total), avg_tags — среднее число тегов на распознанный товар, drop_reasons — сколько раз и по какой причине отброшена ХАРАКТЕРИСТИКА при формировании тегов (раздел 3); привязки «какой товар» здесь нет — это агрегат для калибровки фида в целом, не диагностика конкретной позиции.
Коды прямого вызова:
401— токен не передан, отозван или неверен.403— аккаунт организации заблокирован либо тарифный план не оплачен (доступ по API приостановлен).409— либо пара «Хочу купить» / «Могу продать» временно отключена администрацией сервиса, либо импорт вашего фида уже выполняется (вы отправили два файла разом, или в этот момент сработал импорт по ссылке из раздела 6). Точная причина — в тексте ошибки. В обоих случаях повторите позже: прогоны по одной организации идут строго по очереди, иначе они мешали бы друг другу — в частности, прогон со старым файлом снял бы с публикации товары, которые только что добавил прогон со свежим.413— файл больше 1 МБ.422— файл не разобрался как YAML целиком (например, битый синтаксис) — не путать сskipped/errorsотдельного товара внутри валидного файла.
6. Импорт по ссылке (pull)
Альтернатива прямому вызову — без токена и без запросов с вашей стороны. В блоке «Импорт по ссылке» личного кабинета укажите URL файла (тот же формат, что в разделе 3) — сервис сам периодически его скачивает и синхронизирует намерения тем же конвейером.
- Тумблер «Обновлять автоматически» включает периодическую выкачку; частоту (по умолчанию раз в 6 часов) настраивает поддержка сервиса.
- Тумблер «Закрывать пропавшие из фида» — то же самое, что
remove_missingу прямого вызова, но для pull-режима. - Кнопка «Проверить сейчас» запускает синхронизацию немедленно. Ответ практически всегда
200вида{ "ok": true|false, "feed": {...} }— сам HTTP-код не сигнализирует об отказе (в отличие от прямого вызова, раздел 5): любая ошибка синхронизации (не выкачался файл, битый YAML, отключена пара, не оплачен тариф и т. д.) — этоok: falseи человекочитаемый текст вfeed.last_message, а не отдельный HTTP-код. Исключений два:422, если ссылка на фид ещё не сохранена (сначала заполните и сохраните поле URL), и409, если импорт вашего фида в этот момент уже идёт (например, вы одновременно отправили файл напрямую) — тогда статус фида не меняется, просто дождитесь завершения. При удачеfeed.last_countersсодержит те же счётчики, чтоcountersпрямого вызова (раздел 5). - Ссылка должна быть публичной (обычный http/https, без входа) — сервис не проходит авторизацию на стороне вашего файлового хранилища.
7. Ограничения
- Размер файла — не больше 1 МБ (около 3500 товаров с несколькими характеристиками каждый).
- Территория — по справочнику ФИАС/DaData (регион, город, адрес до дома) или «вся страна».
- Крупный файл обрабатывается не мгновенно: около 15 секунд на 500 товаров, до полутора-двух минут на файл, близкий к лимиту 1 МБ, — это ожидаемо, не сбой. Для очень большого каталога удобнее делить его на несколько файлов поменьше.
- При слишком частых запросах сервис может ответить
429— это защита от перегрузки; сделайте паузу и повторите позже. Для регулярной синхронизации предпочтительнее импорт по ссылке (раздел 6) с разумным интервалом, а не непрерывные повторы вручную. - Матчинг, статусы и остальные лимиты — целиком на стороне сервиса; файл задаёт только исходные данные товара.
Оба способа уже доступны без ожидания — не нужно ничего согласовывать заранее: выпустите ключ или укажите ссылку на файл в личном кабинете организации, разделы «Подключение по API» и «Импорт по ссылке». Вопросы по формату или подключению — через форму «Идея» или на почту поддержки.