О’ПолкаРазместить

API для продавцов

API кабинета продавца позволяет подключить 1С, МойСклад или свою учётную систему: выгружать список товаров, менять цены и остатки, снимать товары с продажи и возвращать их, отмечать продажи, загружать товары пачкой, отвечать на отзывы и сообщения покупателей, забирать статистику. Через API можно ровно то же, что в кабинете, — владение объявлениями, модерация и лимиты проверяются так же; кое-что по ключу недоступно вовсе (см. ниже).

Как получить ключ

  • Кабинет продавца → Настройки → API-ключи для интеграций. Раздел есть только у бизнес-аккаунта.
  • Ключу даётся имя (например, «1С склад»), чтобы потом понимать, какой отзывать. Действующих ключей — не больше 10.
  • Секрет вида plk_… показывается один раз, сразу после выпуска. Потеряли — отзовите ключ и выпустите новый.
  • Отозвать ключ можно там же. Отзыв начинает действовать в течение 5 минут.
  • «Выйти со всех устройств» и смена пароля ключ не отзывают, но прерывают доступ по нему: до 5 минут запросы получают 401 «Требуется вход», потом ключ снова работает. Чтобы закрыть доступ интеграции, ключ нужно отозвать.

Авторизация

Ключ передаётся в каждом запросе заголовком:

Authorization: Bearer plk_…

Ключ работает только с API продавца (адреса, начинающиеся с https://opolka.ru/api/seller) и не заменяет вход на сайте. Храните его как пароль: не кладите в код, который видят покупатели, и не пересылайте в переписке.

Базовый адрес и формат

  • Базовый адрес: https://opolka.ru/api/seller. Все пути ниже — относительно него.
  • Только HTTPS. Тела запросов и ответы — JSON в UTF-8, заголовок Content-Type: application/json для запросов с телом.
  • Ошибки — {"detail": "…"} с текстом по-русски; у ошибок проверки формы (422) detail — список полей.
  • Цены — целые рубли. Даты и время — ISO 8601 (UTC).
  • id товара — число из списка товаров; оно подставляется в путь остальных запросов.

В примерах:

KEY="plk_…"
API="https://opolka.ru/api/seller"

Товары

GET /catalog — список товаров

curl -H "Authorization: Bearer $KEY" \
  "$API/catalog?status=active&per_page=200&page=1"

Параметры (все необязательные):

  • status — all (по умолчанию), active (в продаже), waiting (черновики, на проверке, истёкшие), archive (снятые, проданные, снятые модератором);
  • q — подстрока названия (до 200 символов);
  • sort — created (новые выше, по умолчанию), views, contacts (больше просмотров/контактов за 30 дней выше), price (дешёвые выше);
  • page — с 1; per_page — от 1 до 200, по умолчанию 50.

Ответ:

{
  "items": [
    {
      "id": 123, "title": "Стул дубовый", "price": 4900, "currency": "RUB",
      "stock": 12, "status": "active", "bucket": "active",
      "is_expired": false, "expires_at": "2026-10-25T09:00:00",
      "created_at": "2026-09-25T09:00:00", "city": "Москва",
      "cover_url": "https://…", "seo_path": "/moskva/…/stul-dubovyy_123",
      "views": 340, "favorites": 5, "chats": 3,
      "barcode": null, "moderation_reason": null, "reject_reason": null,
      "metrics_30d": {"views": 120, "phone_reveals": 8, "chats_started": 2}
    }
  ],
  "total": 1, "page": 1, "per_page": 200, "metrics_days": 30,
  "counts": {"all": 40, "active": 31, "waiting": 6, "archive": 3}
}
  • status — состояние объявления: draft, moderation, active, archived, sold, blocked (снято модератором). bucket — вкладка кабинета, по ней работает фильтр status.
  • stock — остаток; null, если остаток не ведётся.
  • variants — варианты товара ([{"label", "price", "stock"}], null без вариантов); price_by_variants — цена товара = минимум цен вариантов, stock_by_variants — остаток = сумма остатков вариантов. Такие цену и остаток быстрой правкой не изменить, а продажа указывает вариант (см. «Цены и остатки» и «Продажи»).
  • total — сколько товаров подходит под фильтр; counts — по вкладкам по всему каталогу, без учёта фильтров.
  • Цена и остаток в списке — актуальные, сразу после правки. metrics_30d (просмотры, показы телефона, начатые переписки за 30 дней) обновляются с задержкой до часа.

GET /catalog/by-barcode — найти товар по штрихкоду

curl -H "Authorization: Bearer $KEY" \
  "$API/catalog/by-barcode?code=4600000000003"
  • code — штрихкод: EAN-8, UPC-A (12 цифр), EAN-13 или GTIN-14; пробелы и дефисы отбрасываются. GTIN-14 с ведущим нулём ищется как EAN-13.
  • Ищутся только ваши товары — по штрихкоду товара и его вариантов, во всех статусах.
  • Ответ: {"code": "4600000000003", "items": […], "metrics_days": 30} — code — код, по которому искали (без пробелов и дефисов), items — товары в том же виде, что в GET /catalog. Не нашлось — пустой items, не 404.
  • Не штрихкод или не сходится контрольная цифра — 400 «Неверный штрихкод».

Цены и остатки

PATCH /catalog/{id}

curl -X PATCH -H "Authorization: Bearer $KEY" \
  -H "Content-Type: application/json" \
  -d '{"price": 1490, "stock": 12}' "$API/catalog/123"
  • Оба поля необязательны: отсутствующее не меняется. Цена — целое число больше нуля (иначе 422), каждое изменение цены попадает в историю цен.
  • Остаток — целое число; отрицательное сохраняется как 0. Объявление с нулевым остатком с витрины само не снимается — снимите его действием archive, если нужно.
  • Если остаток задан у вариантов товара (stock_by_variants: true в карточке), общий остаток — их сумма и здесь не меняется: запрос с stock целиком получит 422, цена из него тоже не сохранится. Так же с ценой у товара с вариантами (price_by_variants: true): она — минимум цен вариантов, запрос с другой price — 422. Совпадающее с текущим значение — не правка, ошибки нет. Цены и остатки вариантов правятся в карточке товара на сайте; варианты коротко (label, price, stock) есть в карточке списка — поле variants.
  • Ответ — карточка товара в том же виде, что в списке (без bucket и metrics_30d). У снятого модератором — 403.

Действия с товаром

curl -X POST -H "Authorization: Bearer $KEY" "$API/catalog/123/archive"
curl -X POST -H "Authorization: Bearer $KEY" "$API/catalog/123/publish"
curl -X POST -H "Authorization: Bearer $KEY" "$API/catalog/123/sold"
curl -X POST -H "Authorization: Bearer $KEY" "$API/catalog/123/duplicate"
curl -X DELETE -H "Authorization: Bearer $KEY" "$API/catalog/123"
  • archive — снять с продажи (в архив, без удаления).
  • publish — опубликовать черновик или вернуть в продажу из архива, продлевает срок размещения. Нужен подтверждённый email аккаунта; незаполненные обязательные поля — 422 «Заполните обязательные поля: …». Текст проходит автопроверку: объявление может уйти на модерацию (status: moderation).
  • sold — отметить проданным целиком: объявление уходит с витрины в архив, записывается продажа одной единицы по текущей цене. Только для активного, иначе 400. Чтобы продать часть остатка и оставить товар в продаже — см. «Продажи».
  • duplicate — копия черновиком (фото, характеристики и остаток переносятся, штрихкод — нет); ответ 201 с карточкой копии.
  • DELETE — удалить навсегда вместе с фото, чатами и продажами по нему; ответ 204 без тела.
  • Действия отвечают карточкой товара. Снятое модератором объявление не публикуется и не архивируется (403).

Массовые действия

POST /catalog/bulk

curl -X POST -H "Authorization: Bearer $KEY" \
  -H "Content-Type: application/json" \
  -d '{"ids": [123, 124, 125], "action": "archive"}' "$API/catalog/bulk"
  • ids — до 100 id за запрос; action — archive, publish, renew (продлить срок активным), delete.
  • Ответ — счётчики: {"done": 2, "skipped": 1}. Пропускаются чужие id и неподходящие по состоянию (архивировать можно только активное, публиковать — черновик или архив с заполненными обязательными полями, продлевать — активное).
  • Какие именно пропущены, ответ не сообщает — сверьте статусы повторным GET /catalog.

Продажи

Продажа фиксируется продавцом — оплаты через площадку нет. Продажа без снятия с витрины уменьшает остаток и попадает в отчёт о выручке.

POST /sales/{id товара} — отметить продажу

curl -X POST -H "Authorization: Bearer $KEY" \
  -H "Content-Type: application/json" \
  -d '{"quantity": 2, "price": 1400}' "$API/sales/123"
  • quantity — от 1 до 10 000, по умолчанию 1; price — цена за единицу, целое ≥ 0, по умолчанию цена варианта (если указан) или текущая цена объявления.
  • variant_label — вариант товара (его label из variants карточки, без учёта регистра, до 40 символов). Если остаток ведётся по вариантам (stock_by_variants: true), вариант обязателен — 422 «Укажите вариант: остаток ведётся по вариантам», и списывается остаток именно этого варианта. Неизвестный вариант — 422 «Варианта «…» нет в объявлении».
  • Только для активного объявления, иначе 400. Если остаток ведётся, он уменьшается на quantity; продать больше остатка (варианта) нельзя — 400 «Продано больше, чем есть в наличии». Объявление остаётся в продаже и при нулевом остатке.
  • Ответ 201: {"id", "listing_id", "price", "quantity", "sold_at", "listing_published_at", "variant_label"}. Сохраните id — он нужен для отмены.

DELETE /sales/{id товара}/{id продажи} — отменить продажу

Ответ 204. Количество возвращается в остаток, если он ведётся; у продажи варианта — в остаток того же варианта. Если вариант с тех пор переименовали или удалили, отмена проходит, а количество не возвращается никуда — поправьте остаток в карточке товара. Отмена продажи, которой товар отмечен «Продано», возвращает его в продажу (как publish); остаток при этом не меняется. Продажа не от этого объявления — 404.

GET /sales?period=30 — отчёт о продажах

period — 7, 30 (по умолчанию) или 90 дней, включая сегодня. Ответ:

{
  "period": 30,
  "revenue": 58800, "sales_count": 14, "units": 21, "avg_check": 4200,
  "prev": {"revenue": 40100, "sales_count": 11, "units": 15, "avg_check": 3645},
  "turnover_days_median": 6,
  "buyout_rate": 0.1167, "contacts": 120,
  "bookings": {"total": 0, "confirmed": 0, "cancelled": 0, "buyout_rate": null},
  "by_day": [{"date": "2026-08-27", "revenue": 0, "sales": 0}, …],
  "items": [{"id": 77, "listing_id": 123, "title": "Стул дубовый",
             "price": 1400, "quantity": 2, "sold_at": "2026-09-25T10:15:00",
             "variant_label": null}]
}
  • revenue — сумма цена × количество; avg_check — выручка на продажу; prev — те же четыре показателя за предыдущий период такой же длины.
  • turnover_days_median — медиана дней от публикации до продажи, null без данных; buyout_rate — продаж на один контакт (показ телефона или переписка), null без контактов.
  • bookings — брони с датой заезда в периоде; null, если данные о бронях временно недоступны.
  • by_day — каждый день периода, дни без продаж — нули; items — до 50 последних продаж периода. Продажи в отчёте видны сразу, отменённые — сразу исчезают.

Импорт товаров из JSON

Импорт создаёт черновики: сначала строки проверяются и сопоставляются со справочниками, потом по готовым строкам создаются объявления. Публикация черновиков — отдельно, через publish или массовое действие.

POST /import/json — разобрать строки

curl -X POST -H "Authorization: Bearer $KEY" \
  -H "Content-Type: application/json" \
  -d @goods.json "$API/import/json"
{
  "filename": "1c-2026-09-25.json",
  "default_city": "Москва",
  "rows": [
    {
      "title": "Стул дубовый",
      "price": 4900,
      "category": "Мебель / Стулья",
      "city": "Москва",
      "description": "Массив дуба",
      "stock": 12,
      "phone": "+79990000000",
      "photos": ["https://example.com/stul-1.jpg", "https://example.com/stul-2.jpg"],
      "attributes": {"Цвет": "Коричневый"}
    }
  ]
}
  • rows — от 1 до 1000 объектов, иначе 422. filename (до 200 символов) — подпись задания в кабинете, необязательна. default_city — город для строк без своего; default_city_id — он же числом из справочника городов сайта (точнее имени: сёл-тёзок в стране сотни).
  • Обязательные поля строки: title (до 200 символов), price (больше нуля; можно строкой «4 900»), category. Остальные — по желанию: city, description (до 5000), stock, phone (пустой — при публикации подставится телефон компании), photos — до 10 ссылок http/https списком или строкой через «|».
  • Ключи можно писать и по-русски, как колонки CSV-шаблона: «Название», «Цена», «Категория», «Город», «Описание», «Остаток», «Телефон», «Фото», «Характеристики». Регистр не важен.
  • category — название («Стулья»), путь через «/», если название встречается в нескольких ветках («Мебель / Стулья»), или адрес категории (slug).
  • city — название города или села, как на сайте. Из одноимённых выбирается город (от 5000 жителей): «Москва» — столица, а не деревня Москва Тверской области. Если город среди тёзок не нашёлся, а сёл с таким именем несколько, строка получает ошибку — уточните регион через запятую: «Ивановка, Оренбургская область».
  • attributes — объект «характеристика → значение». Ключ — название характеристики, как в форме объявления, или её код. Значение из списка должно совпадать с вариантом формы (без учёта регистра; несколько — через «|»), число — числом, да/нет — словом. Неизвестные характеристики пропускаются, неверное значение — ошибка строки.

Ответ 201 — задание со строками (ничего ещё не создано):

{
  "id": 42, "filename": "1c-2026-09-25.json", "status": "parsed",
  "total": 2, "ready": 1, "errors": 1, "created": 0, "failed": 0,
  "created_at": "2026-09-25T10:00:00", "applied_at": null,
  "apply_total": 0, "apply_processed": 0, "apply_error": null,
  "rows": [
    {"id": 1, "line_no": 1, "title": "Стул дубовый", "price": 4900,
     "category_id": 15, "category_name": "Мебель / Стулья",
     "city_id": 1, "city_name": "Москва", "stock": 12,
     "status": "ready", "listing_id": null, "error": null, "warning": null,
     "photos": ["https://example.com/stul-1.jpg"],
     "attributes": [{"name": "Цвет", "value": "Коричневый"}]},
    {"id": 2, "line_no": 2, "title": "Лампа", "price": null, …,
     "status": "error", "error": "Непонятная цена «бесплатно»"}
  ]
}
  • Статус строки: ready — готова, error — ошибка в данных (текст в error), created — черновик создан (listing_id), failed — сайт не принял объявление (текст в error, можно повторить).
  • Статус задания: parsed — есть что применять, applying — черновики создаются, done — всё готовое создано.

POST /import/{id}/apply — создать черновики

curl -X POST -H "Authorization: Bearer $KEY" "$API/import/42/apply"
  • Создаёт черновики по строкам ready и failed. Обычно ответ — 200 и задание в том же виде, уже с результатом по строкам.
  • Большой файл (много строк или медленные фото) может не уложиться в ~25 секунд — тогда ответ 202: задание со status: "applying" и apply_total (сколько строк взято), создание продолжается. Ход и итог — опросом GET /import/{id} раз в несколько секунд: apply_processed — сколько строк уже обработано, статусы строк и счётчики меняются по ходу; когда status перестал быть applying, проход закончен. Не ждать вовсе — POST /import/{id}/apply?wait=0: сразу 202, итог — тем же опросом.
  • Пока проход идёт, повторный apply отвечает 409 «Импорт уже применяется — дождитесь окончания». Проход оборвался (например, перезапуск сервиса) — причина в apply_error, необработанные строки остаются готовыми, apply можно вызвать снова.
  • Повторный вызов не создаёт дублей: трогаются только строки без объявления.
  • Фото скачиваются по ссылкам в этот момент (только публичные адреса, картинка до 10 МБ, ожидание до 10 секунд). Недоступное фото строку не отменяет: черновик создаётся без него, причина — в warning.
  • Создавать можно не больше 10 объявлений в час. Упёрлись в лимит — проход останавливается, остальные строки остаются готовыми; повторите apply через час.

GET /import и GET /import/{id}

Последние 20 заданий (без строк) и одно задание со строками. Чужое задание — 404. Тот же импорт принимает и файлы: POST /import с телом {"filename", "text"} (CSV до 2 МБ) или {"filename", "xlsx_base64"}; шаблон CSV — GET /import/template.

Отзывы

GET /reviews

{
  "avg": 4.8, "count": 25,
  "items": [
    {"id": 501, "rating": 5, "text": "Всё отлично", "created_at": "2026-09-20T12:00:00",
     "author_id": 9, "author_name": "Анна", "target_id": 3, "target_name": "ООО Ромашка",
     "listing_id": 123, "listing_title": "Стул дубовый", "listing_seo_path": "/moskva/…",
     "reply": null, "reply_at": null}
  ]
}

Отзывы о вашей компании, новые сверху. Без ответа — reply: null.

POST /reviews/{id}/reply

curl -X POST -H "Authorization: Bearer $KEY" \
  -H "Content-Type: application/json" \
  -d '{"text": "Спасибо за покупку!"}' "$API/reviews/501/reply"
  • Ответ на отзыв один; сохраняются первые 2000 символов (текст длиннее 4000 — 422). Пустой text или null — снять ответ.
  • Изменить или снять ответ можно в течение суток после первой публикации, дальше — 403. Текст проходит автопроверку (400 с причиной).
  • Ответ — отзыв целиком в том же виде, что в списке.

Брони

Брони есть у объявлений посуточного жилья.

GET /bookings

[
  {"id": 31, "date_from": "2026-10-01", "date_to": "2026-10-05",
   "status": "pending", "past": false,
   "listing_id": 123, "listing_title": "Дом у моря",
   "guest_id": 9, "guest_name": "Анна"}
]

status — pending (ждёт решения), confirmed, cancelled; past — дата выезда прошла. Ожидающие — сверху, прошедшие и отменённые — в конце.

POST /bookings/{id}/confirm · POST /bookings/{id}/cancel

Подтвердить или отменить бронь; гость получает сообщение в чате. Ответ — {"id": 31, "status": "confirmed"}. Повтор того же действия безопасен; подтвердить отменённую — 409.

Чаты с покупателями

GET /chats

Переписки по вашим объявлениям (где вы — продавец), свежие сверху:

[
  {"id": 88, "listing_id": 123, "listing_title": "Стул дубовый",
   "listing_price": 4900, "listing_cover": "https://…", "listing_seo_path": "/moskva/…",
   "role": "seller", "is_support": false,
   "peer_id": 9, "peer_name": "Анна", "peer_avatar": null,
   "last_message": "Здравствуйте, ещё продаёте?", "last_at": "2026-09-25T09:40:00",
   "unread": 1}
]

GET /chats/{id}

Переписка — {"chat": {…как в списке}, "messages": [...]}, последние 200 сообщений по порядку: {"id", "sender_id", "body", "deleted", "created_at", "read_at"}. Ваши сообщения — те, где sender_id не равен peer_id. Запрос помечает входящие прочитанными, как открытие переписки на сайте.

POST /chats/{id}/messages

curl -X POST -H "Authorization: Bearer $KEY" \
  -H "Content-Type: application/json" \
  -d '{"body": "Да, в наличии"}' "$API/chats/88/messages"

body — до 2000 символов, пустое — 400. Ответ — отправленное сообщение. Не больше 30 сообщений в минуту; если собеседник вас заблокировал — 403. Шаблоны быстрых ответов — GET/POST /chats/templates, DELETE /chats/templates/{id}.

Статистика

Данные обновляются раз в час. period — строго 7, 30 (по умолчанию) или 90, иначе 422; период заканчивается сегодняшним днём (UTC).

  • GET /summary?period=30 — сводка: views, unique_visitors, phone_reveals, chats_started, favorites (сколько раз товары сейчас в избранном), prev — то же за прошлый период, updated_at — время последнего обновления, spark — ряды за 7 дней, catalog (активные, черновики, истекают за 7 дней, без фото, всего) и chat (переписок, доля отвеченных, медиана первого ответа в минутах) — последние два null до первого обновления.
  • GET /funnel?period=30 — воронка {"steps": [{"key", "title", "value"}], "conversions": {"views_to_phone", "phone_to_chat"}}.
  • GET /top?period=30&limit=10 — by_views, by_contacts ({listing_id, views, phone_reveals}) и silent — просмотры есть, контактов нет; limit 1–50.
  • GET /timeseries?period=30&metric=views — {"points": [{"date", "value"}]} по дням, дни без данных — нули; metric — views, phone_reveals, chats_started.
  • GET /export?period=30 — CSV (разделитель «;», UTF-8 с BOM): дата, id товара, источник, просмотры, уникальные посетители, показы телефона, переписки, избранное.
  • GET /rating — рейтинг продавца 0–100 по скорости ответов, отменам броней, модерации и отзывам, с советами, что улучшить.
  • GET /me — компания и счётчики: неотвеченные отзывы, непрочитанные чаты, брони на решении, черновики, объявления без фото и истекающие. Счётчик, который не удалось получить, приходит null.

Лимиты

  • 600 запросов в минуту на ключ по всем адресам вместе. Сверх — 429 «Слишком много запросов по ключу — не больше 600 в минуту»; дождитесь конца минуты.
  • Не больше 20 запросов в секунду с одного IP-адреса — сверх приходит 429, иногда без JSON-тела.
  • Правила сайта действуют как в кабинете: создание объявлений (импорт, копии) — 10 в час, публикация по одному — 20 в час, отметки продаж — 60 в минуту, сообщения — 30 в минуту. Их 429 приходит с текстом «Слишком много запросов — попробуйте позже».
  • Размеры: до 200 товаров на странице списка, до 100 id в массовом действии, до 1000 строк и до 10 фото на строку в импорте.

Коды ошибок

Код Когда
401 «Требуется вход» Нет заголовка Authorization или он не вида Bearer plk_…; либо в аккаунте только что вышли со всех устройств или сменили пароль — ключ снова заработает в течение 5 минут
401 «Ключ недействителен» Ключ неверный или отозван, аккаунт заблокирован или перестал быть бизнес-аккаунтом. Причина намеренно не уточняется
403 «Недоступно по API-ключу» Раздел настроек и безопасности — см. «Что недоступно по ключу»
403 «Раздел доступен бизнес-аккаунтам» Аккаунт больше не компания
403 «Это не ваше объявление» и подобные Объект принадлежит другому пользователю или действие запрещено правилами (например, объявление снято модератором)
404 Объект не найден: объявление, отзыв, бронь, продажа, задание импорта
400 / 409 Нарушено правило площадки: «Продано больше, чем есть в наличии», «Бронь уже отменена» и т. п. — текст в detail
422 Ошибка в теле или параметрах. У проверки формы detail — список полей с описанием, у правил площадки — строка
429 Превышен лимит (см. «Лимиты») — подождите и повторите
502 «Основной сервис недоступен…» / 503 «Кабинет временно недоступен…» Временный сбой на нашей стороне — повторите позже с паузой

Что недоступно по ключу

Настройки компании и безопасность: профиль компании и логотип, смена пароля, «выйти со всех устройств», список входов и их завершение, возврат на личный аккаунт, удаление аккаунта, повтор письма подтверждения, принятие оферты и проверка ИНН — и сами API-ключи: выпускать и отзывать их можно только в кабинете. Так утёкший ключ не даёт захватить аккаунт или выпустить себе новые ключи. На всё это API отвечает 403 «Недоступно по API-ключу».

Советы по синхронизации

  • Сопоставьте товары учётной системы с id один раз (по названию или штрихкоду из списка) и храните связку у себя — дальше работайте по id. Штрихкод уникален в пределах города: если товар продаётся в нескольких городах, у него по объявлению на город с одним и тем же кодом.
  • Остатки и цены синхронизируйте раз в несколько минут, а не в непрерывном цикле, и отправляйте только изменившиеся позиции.
  • Весь каталог читайте страницами по 200 (per_page=200), пока не наберётся total.
  • Продажи из 1С отмечайте через POST /sales/{id}, а не правкой остатка: так они попадут в отчёт о выручке, а остаток уменьшится сам. У товара с остатком по вариантам передавайте variant_label.
  • На 429, 502 и 503 повторяйте с растущей паузой (например, 5, 15, 60 секунд). 401 «Ключ недействителен» повтором не лечится — выпустите новый ключ; 401 «Требуется вход» при верном заголовке проходит сам за несколько минут.
  • Новые товары загружайте импортом: разберите строки, исправьте ошибки, примените, затем опубликуйте черновики массовым publish; если skipped больше нуля — найдите непубликованные в GET /catalog?status=waiting.

Вопросы по интеграции — support@opolka.ru.