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/xml

# тело — сам XML-файл целиком, БЕЗ JSON-обёртки (см. формат ниже)

В счётчиках отчёта есть no_photo — сколько товаров пришло без единой картинки. Такой товар публикуется как обычно, просто без фотографии: положить её можем только вы, добавив <picture> в выгрузку. Товары, у которых ссылка на картинку есть, но не открывается, в это число не входят.

Там же desc_truncated — у скольких товаров описание пришлось сократить: описание намерения вмещает 6000 символов вместе с названием товара. Товар при этом публикуется, сокращение идёт по границе абзаца или строки.

dry_run=1 — сухой прогон: тот же отчёт, что и боевой импорт, но без создания и изменения намерений (пригодится, чтобы проверить файл перед первой загрузкой). remove_missing=1 — закрыть намерения, чьи товары пропали из текущего файла (без параметра пропавшие товары остаются как есть).

3. Формат файла: YML Яндекс.Маркета

Сервис принимает один формат — YML Яндекс.Маркета (XML), тот самый файл, который у вас, скорее всего, уже есть: его выгружают 1С-Битрикс, InSales и другие системы, обычно по адресу вида /bitrix/catalog_export/yandex_НОМЕР.php. Ничего специально готовить не нужно — дайте ссылку на существующую выгрузку либо проверьте файл сухим прогоном перед загрузкой. Формат определяется по содержимому файла, расширение ссылки и заголовок Content-Type значения не имеют; файл в любом другом формате сервис не примет (раздел 5, код 422).

Что сервис берёт из YML: название товара, описание <description>, цену, категорию (путь по дереву <categories> собирается автоматически по <categoryId>), производителя <vendor>, характеристики <param> и изображения <picture> — картинки скачиваются к нам и показываются на карточке намерения (ссылки на ваш сайт мы не сохраняем: они перестанут работать, если вы переставите файлы). Берётся несколько первых картинок товара — сколько именно, настраивает оператор сервиса, по умолчанию три. Перенос идёт фоном после импорта, поэтому фотографии появляются на несколько минут позже самих намерений. Если какие-то картинки не доехали (ваш сервер был недоступен, ссылка временно не работала), сервис доберёт их при следующем прогоне фида — заново загружать выгрузку не нужно. Одна и та же картинка, поставленная у нескольких товаров, хранится у нас один раз и достаётся всем им. Товары с available="false" пропускаются — намерение «могу продать» о том, чего нет на складе, никому не поможет. Кодировка windows-1251, в которой Битрикс выгружает по умолчанию, распознаётся сама.

Файл — список товаров вашего каталога, а не готовых намерений: сервис сам решает, во что превратить каждый товар. Как поле обязателен только атрибут id у <offer> — без него запись не свяжется с намерением, сервис вернёт по товару ошибку и не создаст черновик. <categoryId> тоже нужен практически всегда: без категории товар не пропадает с ошибкой всего запроса, но пропускается (раздел 5). Дальше — не поля, а требования к результату: импорт всегда создаёт намерения «Могу продать», а для этого типа по умолчанию обязательны цена и хотя бы один тег, который узнает словарь характеристик — оба разобраны в подсказках под примером.

<?xml version="1.0" encoding="utf-8"?>
<yml_catalog>
  <shop>
    <name>Мой магазин</name>
    <categories>
      <category id="40">Инструменты</category>
      <category id="50" parentId="40">Электроинструмент</category>
      <category id="60" parentId="50">Дрели-шуруповёрты</category>
      <category id="10">Женщинам</category>
      <category id="20" parentId="10">Женская одежда</category>
      <category id="30" parentId="20">Платья</category>
    </categories>
    <offers>
      <offer id="sku-101">
        <name>Дрель-шуруповёрт Bosch GSR 12V-15, 2 АКБ, кейс</name>
        <description><![CDATA[<p>Компактная дрель для работы <b>в ограниченном пространстве</b>.</p>
<h3>В комплекте</h3>
<ul><li>два аккумулятора</li><li>зарядное устройство</li><li>кейс</li></ul>]]></description>
        <categoryId>60</categoryId>
        <vendor>Bosch</vendor>
        <price>8990</price>
        <param name="Тип аккумулятора">Li-Ion</param>
        <param name="Наличие реверса">да</param>
        <param name="Цвет">синий</param>
      </offer>
      <offer id="sku-102">
        <name>Платье летнее хлопковое, синее, 44 размер</name>
        <categoryId>30</categoryId>
        <vendor>Zarina</vendor>
        <price>2490</price>
        <param name="Размер">44</param>
        <param name="Цвет">синий</param>
      </offer>
    </offers>
  </shop>
</yml_catalog>
  • id (атрибут <offer id="…">) — ваш идентификатор товара (он же external_id), обязателен: без него сервис не создаст запись, а вернёт по этому товару ошибку «у товара нет id (в YML обязателен атрибут <offer id>)» в errors отчёта (раздел 5) — в намерение он не превратится.
  • categoryId — ссылка на категорию из дерева <categories>; путь сервис собирает сам по вложенности parentId. Категория — источник тегов и запасное название намерения на случай, когда из названия товара взять нечего: знакомую сервису категорию он переводит по словарю (лист «Дрели-шуруповёрты» → «дрель-шуруповёрт»), незнакомую — берёт по её имени. Товар без категории или с пустым именем категории пропускается (попадает в skipped отчёта, раздел 5, с причиной «пустая категория»).
  • name — обычное название товара из вашего каталога. Из него сервис берёт НАЗВАНИЕ намерения — одно-два значимых слова, то, что покупатель наберёт в своём «хочу купить» («Дрель-шуруповёрт Bosch GSR 12V-15…» → «дрель-шуруповёрт»). Полное название целиком становится первым абзацем описания намерения: модель и размер нужны покупателю, а в коротком названии намерения им нет места.
  • description — описание товара, необязательно. Оно идёт в описание намерения следом за названием. Годится и обычный текст, и HTML — внутри <![CDATA[ … ]]>, экранированный или просто тегами. Сохраняется оформление: абзацы и переносы, подзаголовки (<h1><h6> — одного уровня), списки, жирный и курсив; строки, начатые «- » или «• », тоже становятся пунктами списка. Ссылки остаются текстом без адреса, стили, цвета и размеры шрифта отбрасываются, картинки и скрипты удаляются целиком. Если описание уже начинается с названия товара, название второй раз не добавляется. Описание намерения вмещает 6000 символов — длиннее сокращается по границе абзаца, и это видно по счётчику desc_truncated (раздел 2).
  • vendor и param (характеристика в теге <param name="…">…</param>) — становятся тегами намерения по внутреннему словарю, но только при ТОЧНОМ (без учёта регистра) совпадении имени характеристики со словарём: например, «Наличие реверса: да» → тег «с реверсом», а вот просто «Реверс: да» словарь не узнает по имени и молча отбросит. У каждого узнанного тега есть вес важности (зависит от категории товара, домена и самой характеристики), и в намерение он попадает, только если вес не ниже порога отсечения — например, «Цвет» у инструментов намеренно снижен (для подбора совпадений по дрели цвет не важен) и почти всегда отбрасывается, а у одежды, наоборот, входит в вес. В примере выше поэтому «Наличие реверса: да» у первого товара становится тегом «с реверсом», а «Цвет: синий» у него же — отбрасывается по весу; у второго товара (одежда) «Цвет: синий» тегом становится. Тегов на намерение остаётся немного (алгоритм подбора совпадений по тегам точнее работает с коротким набором самых важных, а не длинным списком характеристик). Отброшенное — не ошибка: агрегированную статистику (сколько раз и по какой причине) показывает поле metrics.drop_reasons отчёта (раздел 5); привязки «какой именно товар и характеристика» в отчёте нет — это сводка по всему фиду.
  • Если характеристик (<param>) нет вовсе — а в выгрузках магазинов так чаще всего и бывает, — теги собираются из НАЗВАНИЯ товара: значимые слова, латинская марка и размеры. Размер понимается в трёх привычных формах: 8х 60 → тег 8х60, М 14,0м14, d 3 и Ду 20d3 и ду20. Это важно для крепежа и сантехники: без размера сотни болтов выглядели бы для подбора совпадений одинаково. Артикул в начале названия («D 46 Колесо») тегом не становится — его никто не ищет.
  • Товар без единого тега не пропадёт. Если после разбора тегов не нашлось (короткое название вроде «Засов гаражный», где оба слова ушли в название намерения), тегом станет слово самого названия. Раньше такой товар не импортировался вовсе с причиной «укажите хотя бы один тег»; теперь он публикуется, просто набор тегов у него беднее.
  • Территория — только из профиля организации. YML город не несёт, поэтому сервис берёт город из профиля организации (раздел «Профиль» личного кабинета). Проще всего задать город один раз там — он подхватится для всех товаров. Если города в профиле нет, товар НЕ импортируется — в отчёте будет причина «не указан город»: «Вся страна» совпадает только со «Всей страной» (см. «Как это работает» → «Территория»), то есть импорт без города плодил бы намерения, не способные ни с кем совпасть.
  • price — цена в рублях. Фактически ОБЯЗАТЕЛЬНА: импорт всегда создаёт намерения «Могу продать», а для этого типа по умолчанию нужна цена больше нуля (шаг «Стоимость» мастера обязателен, пока администратор сервиса явно не отключит его для этого типа в настройках конструктора мастера — по умолчанию включён). Импорт никогда не проставляет «цена договорная» автоматически — если price не указан или ≤ 0, сервис не создаст намерение и вернёт по товару ошибку «price: укажите стоимость или «договорная»» в errors отчёта (раздел 5).

Все загруженные товары становятся намерениями «Могу продать» — другие типы (аренда, услуги, вакансии) через этот канал сейчас недоступны. Количество — не отдельное поле оффера, а тоже характеристика в <param>: сервис ищет среди них ЛЮБУЮ с именем, содержащим «количество в упаковке» (регистр не важен), и её числовое значение становится количеством намерения — единица берётся из суффикса имени через запятую (например, «Количество в упаковке, шт» → 200 шт.; без суффикса единица останется не указана). Нет такой характеристики или её значение не число — количество принимается за 1 шт. Состояние товара при импорте всегда «новое» — это не настраивается.

4. Синхронизация и обновления

  • id связывает запись файла с намерением в сервисе: повторная загрузка того же id обновляет намерение на месте (без версий, как и везде в сервисе), а не создаёт дубль.
  • Товар, пропавший из очередного файла, сам по себе никак не меняется — намерение остаётся опубликованным. Чтобы такие товары закрывались автоматически, добавьте remove_missing=1 к запросу (push) или включите тумблер «Закрывать пропавшие из фида» (импорт по ссылке).
  • Файл без товаров мы не применяем. Если файл прочитался, но товаров в нём нет, загрузка отменяется целиком с ошибкой «фид не содержит товаров» — опубликованные намерения остаются на месте. Это защита от сбоя вашего сервера: отдай он пустой ответ или страницу ошибки, включённый флажок «Закрывать пропавшие из фида» снял бы с показа весь ваш каталог, а вернуть намерения в показ пришлось бы вручную по одному.
  • Фотографии и видео, добавленные вручную, импорт больше не стирает (с 09.08.2026). Файл фида их не содержит и никогда не содержал, поэтому единственный способ показать фото у загруженного товара — добавить его самим. Раньше любая последующая загрузка с изменённой ценой или описанием снимала эти фото: сам файл в хранилище оставался, но переставал быть привязан к намерению. Теперь загрузка меняет только те поля, которые есть в файле.
  • Загруженные намерения проходят те же проверки, что и созданные вручную: нормализация тегов, фильтр недопустимых слов, лимиты активных намерений — и публикуются сразу.

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 — файл больше действующего предела размера.
  • 422 — файл не похож на YML Яндекс.Маркета (нет XML-корня yml_catalog, битый синтаксис, вообще не XML) — не путать с skipped/errors отдельного товара внутри валидного файла.

6. Импорт по ссылке (pull)

Альтернатива прямому вызову — без токена и без запросов с вашей стороны. В блоке «Импорт по ссылке» личного кабинета укажите URL файла (тот же формат, что в разделе 3) — сервис сам периодически его скачивает и синхронизирует намерения тем же конвейером.

  • Тумблер «Обновлять автоматически» включает периодическую выкачку; частоту (по умолчанию раз в 6 часов) настраивает поддержка сервиса.
  • Тумблер «Закрывать пропавшие из фида» — то же самое, что remove_missing у прямого вызова, но для pull-режима.
  • Кнопка «Проверить сейчас» запускает синхронизацию немедленно. Ответ практически всегда 200 вида { "ok": true|false, "feed": {...} } — сам HTTP-код не сигнализирует об отказе (в отличие от прямого вызова, раздел 5): любая ошибка синхронизации (не выкачался файл, файл не YML, отключена пара, не оплачен тариф и т. д.) — это ok: false и человекочитаемый текст в feed.last_message, а не отдельный HTTP-код. Исключений два: 422, если ссылка на фид ещё не сохранена (сначала заполните и сохраните поле URL), и 409, если импорт вашего фида в этот момент уже идёт (например, вы одновременно отправили файл напрямую) — тогда статус фида не меняется, просто дождитесь завершения. При удаче feed.last_counters содержит те же счётчики, что counters прямого вызова (раздел 5).
  • Ссылка должна быть публичной (обычный http/https, без входа) — сервис не проходит авторизацию на стороне вашего файлового хранилища.

7. Ограничения

  • Размер файла — по умолчанию не больше 60 МБ, товаров в файле — не больше 60 000. Оба предела настраиваются оператором сервиса, поэтому текущие значения уточняйте в поддержке; при превышении приходит 413 с указанием предела.
  • Территория — по справочнику ФИАС/DaData (регион, город, адрес до дома) или «вся страна».
  • Крупный файл обрабатывается не мгновенно: само чтение файла быстрое (около полусекунды на 20 тысяч товаров), но дальше каждый товар проходит проверки и запись, и это занимает время — до полутора-двух минут на большой каталог. Это ожидаемо, не сбой.
  • При слишком частых запросах сервис может ответить 429 — это защита от перегрузки; сделайте паузу и повторите позже. Для регулярной синхронизации предпочтительнее импорт по ссылке (раздел 6) с разумным интервалом, а не непрерывные повторы вручную.
  • Матчинг, статусы и остальные лимиты — целиком на стороне сервиса; файл задаёт только исходные данные товара.

Оба способа уже доступны без ожидания — не нужно ничего согласовывать заранее: выпустите ключ или укажите ссылку на файл в личном кабинете организации, разделы «Подключение по API» и «Импорт по ссылке». Вопросы по формату или подключению — в чат «Идеи и предложения» (первый в списке ваших чатов) или на почту поддержки.

Если вы перестали поставлять фид

Убрали ссылку или выключили фид — сервис считает это остановкой поставки. Ваши намерения из этого фида останутся в выдаче ещё неделю, а потом будут сняты с публикации. Точную дату вы увидите в личном кабинете, в блоке «Импорт по ссылке», и получите уведомление в момент остановки.

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

Три вещи, которые стоит знать:

  • это обратимо. Вернули ссылку и включили фид до истечения срока — снятие отменяется, ничего делать не нужно;
  • намерения не удаляются, а снимаются. История и диалоги сохраняются; чтобы вернуть их в выдачу позже, укажите ссылку и запустите импорт;
  • намерения, созданные вами вручную, не трогаются. Правило касается только того, что пришло из фида.

Если вы просто меняете адрес фида — недели с запасом хватает, и делать ничего не надо.