Lootmarket
REST API v1

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

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

Базовый URL: https://45-88-14-102.sslip.io/api/v1

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

  1. Создайте ключ в кабинете → API и вебхуки. Ключ показывается один раз.
  2. Найдите категорию для своих товаров через GET /games и её поля через GET /games/{slug}.
  3. Загрузите объявления одним запросом POST /listings/bulk с вашими external_id и повторяйте его при каждой синхронизации.
  4. Для автовыдачи пополняйте склад POST /listings/{id}/stock, для ручной — подпишитесь на вебхук deal.paid.
Проверка ключа
curl https://45-88-14-102.sslip.io/api/v1/me \
  -H "Authorization: Bearer mk_ваш_ключ"

Аутентификация

Каждый запрос подписывается API-ключом вида mk_…. Передайте его в заголовке Authorization: Bearer <ключ> или X-Api-Key: <ключ>. Мы храним только хеш ключа; в кабинете виден префикс, дата создания и последнего использования. Скомпрометированный ключ отзовите в кабинете — он перестанет работать сразу.

У ключа есть права (scopes). Запрос без нужного права получит 403.

ScopeЧто разрешает
listings:readЧтение объявлений и склада
listings:writeСоздание и изменение объявлений, цен и склада
deals:readЧтение сделок и чатов
deals:writeВыдача товара и сообщения в чатах сделок

GET /me, GET /games и GET /games/{slug} доступны любому действующему ключу.

Формат ответов и ошибок

Все ответы — JSON в UTF-8. Успешный ответ содержит data и, для списков и пакетных операций, meta. Поля — в snake_case, даты — ISO 8601 в UTC.

Успех
{
  "data": { "id": "cm1…", "title": "1000 V-Bucks", "price_kopecks": 49900, "price_rub": "499.00" },
  "meta": { "limit": 50, "has_more": false, "next_cursor": null }
}
Ошибка
{
  "error": {
    "code": "validation_error",
    "message": "price_kopecks: Минимальная цена — 1 ₽",
    "details": [{ "path": "price_kopecks", "message": "Минимальная цена — 1 ₽" }]
  }
}
HTTPcodeКогда
400bad_request, invalid_jsonНекорректные параметры или тело запроса не JSON
401unauthorizedНет ключа, ключ неверный или отозван
403forbiddenУ ключа нет нужного scope или действие запрещено
404not_foundОбъект не найден или принадлежит другому пользователю
409conflictexternal_id уже занят другим объявлением
422validation_error, unprocessableДанные не прошли проверку или действие недопустимо в текущем статусе
429rate_limitedПревышен лимит запросов
500internal_errorОшибка на нашей стороне — повторите позже

Лимиты и пагинация

Не больше 120 запросов в минуту на ключ. Каждый ответ содержит заголовки X-RateLimit-Limit, X-RateLimit-Remaining и X-RateLimit-Reset (unix-время сброса окна). При превышении — 429 и Retry-After в секундах. Для массовых операций используйте пакетные эндпоинты: один bulk на 500 объявлений стоит одного запроса.

Списки постраничные: limit от 1 до 100 (по умолчанию 50) и cursor. Следующую страницу запрашивайте с cursor=meta.next_cursor, пока meta.has_more не станет false.

curl "https://45-88-14-102.sslip.io/api/v1/listings?status=ACTIVE&limit=100&cursor=cm1abc…" -H "Authorization: Bearer $KEY"
ОперацияПредел за запрос
POST /listings/bulk500 объявлений
PATCH /listings/prices1000 позиций
POST /listings/{id}/stock5000 строк, каждая до 2000 символов
POST /api/uploadфайл до 8 МБ, 30 загрузок в минуту

Деньги

Все суммы — целые копейки в полях *_kopecks. Для удобства рядом дублируется строка в рублях *_rub с двумя знаками после точки. При записи можно передать любое из двух: price_kopecks: 49900 или price_rub: "499.00" (также "499,00" и число 499). Если переданы оба — главнее price_kopecks.

Профиль и каталог

Профиль и баланс

GET/api/v1/me
Ответ
{
  "data": {
    "id": "cm0…", "username": "shop_best", "rating": 4.9, "sales_count": 1520,
    "balance_kopecks": 1250000, "balance_rub": "12500.00",
    "frozen_balance_kopecks": 34900, "frozen_balance_rub": "349.00",
    "active_listings": 87, "open_sales": 3,
    "api_key": { "prefix": "mk_AbC12dE", "scopes": ["listings:read", "listings:write", "deals:read", "deals:write"] }
  }
}

Список игр

GET/api/v1/games?type=GAME&q=fortnite

type — GAME, MOBILE_GAME или APPLICATION; q — поиск по названию. В ответе у каждой игры есть список категорий с id — он нужен для создания объявлений.

Игра с полями категорий

GET/api/v1/games/{slug}

Возвращает категории и их поля (fields). Значения полей передаются в объявлении в объекте attributes по ключу key. Для SELECT значение должно совпадать с одним из options, для NUMBER — число, поля с required: true обязательны.

Ответ
{
  "data": {
    "id": "cm0…", "slug": "fortnite", "name": "Fortnite", "type": "GAME",
    "categories": [{
      "id": "cm0cat…", "slug": "v-bucks", "name": "В-баксы",
      "fields": [
        { "key": "platform", "label": "Платформа", "type": "SELECT", "options": ["PC", "PlayStation", "Xbox"], "required": true },
        { "key": "amount", "label": "Количество", "type": "NUMBER", "options": [], "required": false }
      ]
    }]
  }
}

Объявления

Объявление принадлежит владельцу ключа; чужие объявления для API не существуют (404). Если на площадке включена модерация, новое объявление получает статус PENDING, а изменение цены, заголовка или категории активного объявления отправляет его на повторную проверку.

ПолеТипОписание
category_idstringКатегория из /games — обязательно при создании
titlestring3–120 символов
descriptionstringдо 5000 символов
price_kopecks | price_rubint | stringЦена за 1 шт., от 1 ₽ до 1 000 000 ₽
old_price_kopecksint | nullЗачёркнутая цена, должна быть больше текущей
quantityintОстаток для MANUAL; для AUTO считается по складу
delivery_typeMANUAL | AUTOРучная выдача в чате или автовыдача со склада
delivery_infostringИнструкция покупателю после оплаты
attributesobjectЗначения полей категории {key: value}
imagesstring[]До 10 ссылок: из POST /api/upload или внешние https
external_idstringВаш id товара, уникален в пределах продавца
stockstring[]Только AUTO: строки, которые добавятся на склад

Список объявлений

GET/api/v1/listings?status=ACTIVE,PAUSED&categoryId=&external_id=&cursor=&limit=listings:read

status и external_id принимают несколько значений через запятую.

Создать объявление

POST/api/v1/listingslistings:write
curl
curl -X POST https://45-88-14-102.sslip.io/api/v1/listings \
  -H "Authorization: Bearer $KEY" -H "Content-Type: application/json" \
  -d '{
    "external_id": "vbucks-1000-pc",
    "category_id": "cm0cat…",
    "title": "1000 V-Bucks на ваш аккаунт",
    "description": "Пополнение через подарочный код, 5 минут.",
    "price_rub": "499.00",
    "quantity": 50,
    "delivery_type": "MANUAL",
    "attributes": { "platform": "PC" },
    "images": ["/uploads/listings/3f9c….webp"]
  }'
Ответ 201
{
  "data": {
    "id": "cm1…", "short_id": "6992fc5806ba", "external_id": "vbucks-1000-pc",
    "url": "https://45-88-14-102.sslip.io/products/6992fc5806ba-1000-v-bucks-na-vash-akkaunt",
    "status": "ACTIVE", "price_kopecks": 49900, "price_rub": "499.00",
    "old_price_kopecks": null, "old_price_rub": null, "quantity": 50, "delivery_type": "MANUAL", …
  }
}

Получить, изменить, удалить

GET/api/v1/listings/{id}listings:read
PATCH/api/v1/listings/{id}listings:write
DELETE/api/v1/listings/{id}listings:write

Вместо id можно передать short_id. PATCH меняет только переданные поля. DELETE удаляет объявление; если по нему уже были сделки — переводит в архив (archived: true), а при незавершённых сделках возвращает 422.

curl -X PATCH https://45-88-14-102.sslip.io/api/v1/listings/cm1… -H "Authorization: Bearer $KEY" \
  -H "Content-Type: application/json" -d '{"price_kopecks": 45900, "old_price_kopecks": 49900}'

Сменить статус

POST/api/v1/listings/{id}/statuslistings:write

{"status": "PAUSED"} — снять с витрины, ACTIVE — вернуть, ARCHIVED — в архив. Приостановить можно только активное объявление; объявление на модерации или отклонённое включить нельзя (422).

Массовая загрузка и синхронизация (bulk upsert)

POST/api/v1/listings/bulklistings:write

До 500 объявлений за запрос. external_id обязателен — это ключ идемпотентности: если у вас уже есть объявление с таким external_id, оно обновится (переданные поля поверх текущих), иначе создастся новое. Повтор того же запроса не создаёт дублей, поэтому синхронизацию можно запускать по расписанию. Ошибка в одном элементе не отменяет остальные — результат приходит по каждому. Необязательное поле status (ACTIVE | PAUSED | ARCHIVED) позволяет заодно снять с продажи товары, которых у вас больше нет.

curl
curl -X POST https://45-88-14-102.sslip.io/api/v1/listings/bulk \
  -H "Authorization: Bearer $KEY" -H "Content-Type: application/json" \
  -d '{
    "items": [
      { "external_id": "sku-1001", "category_id": "cm0cat…", "title": "Аккаунт с 50 скинами",
        "price_kopecks": 150000, "delivery_type": "AUTO", "attributes": { "platform": "PC" },
        "stock": ["login1:pass1", "login2:pass2"] },
      { "external_id": "sku-1002", "category_id": "cm0cat…", "title": "2800 V-Bucks",
        "price_rub": 1290, "quantity": 20, "attributes": { "platform": "PC" } },
      { "external_id": "sku-0999", "status": "ARCHIVED" }
    ]
  }'
Ответ 200
{
  "data": [
    { "index": 0, "external_id": "sku-1001", "result": "created", "id": "cm1…", "short_id": "a1b2c3d4e5f6", "status": "ACTIVE" },
    { "index": 1, "external_id": "sku-1002", "result": "updated", "id": "cm1…", "short_id": "0f9e8d7c6b5a", "status": "ACTIVE" },
    { "index": 2, "external_id": "sku-0999", "result": "error",
      "error": { "code": "unprocessable", "message": "Выберите категорию" } }
  ],
  "meta": { "total": 3, "created": 1, "updated": 1, "errors": 1 }
}

Для обновления существующего объявления достаточно external_id и изменившихся полей. Чтобы только архивировать объявление, используйте POST /listings/{id}/status или передайте его полные данные вместе со status. Строки из stock при обновлении добавляются на склад (дубли пропускаются), а не заменяют его.

Массовое изменение цен

PATCH/api/v1/listings/priceslistings:write

До 1000 позиций. Объявление указывается через id или external_id. Если новая цена не ниже старой зачёркнутой и old_price_kopecks не передан, зачёркнутая цена снимается.

curl -X PATCH https://45-88-14-102.sslip.io/api/v1/listings/prices -H "Authorization: Bearer $KEY" \
  -H "Content-Type: application/json" \
  -d '{"items": [
        {"external_id": "sku-1001", "price_kopecks": 139000},
        {"id": "cm1…", "price_rub": "99.90", "old_price_kopecks": 14900}
      ]}'
Ответ
{
  "data": [
    { "index": 0, "id": "cm1…", "external_id": "sku-1001", "result": "updated", "price_kopecks": 139000, "price_rub": "1390.00", "status": "ACTIVE" },
    { "index": 1, "id": "cm1…", "external_id": null, "result": "updated", "price_kopecks": 9990, "price_rub": "99.90", "status": "ACTIVE" }
  ],
  "meta": { "total": 2, "updated": 2, "errors": 0 }
}

Склад автовыдачи

Для объявлений с delivery_type: "AUTO" товар выдаётся покупателю сразу после оплаты — одна строка склада на единицу товара (код, логин:пароль, ссылка). Остаток quantity такого объявления всегда равен числу непроданных строк. Пустые строки и дубликаты (в запросе и на складе) пропускаются. Если объявление было распродано, пополнение склада снова делает его активным.

POST/api/v1/listings/{id}/stocklistings:write
Добавить коды
curl -X POST https://45-88-14-102.sslip.io/api/v1/listings/cm1…/stock \
  -H "Authorization: Bearer $KEY" -H "Content-Type: application/json" \
  -d '{"items": ["XXXX-YYYY-ZZZZ-0001", "XXXX-YYYY-ZZZZ-0002", "XXXX-YYYY-ZZZZ-0003"]}'
Ответ 201
{ "data": { "added": 3, "skipped": 0, "in_stock": 42 } }
GET/api/v1/listings/{id}/stock?sold=false&limit=500&offset=0listings:read
Ответ
{
  "data": [
    { "id": "cm2…", "content": "XXXX-YYYY-ZZZZ-0001", "is_sold": false, "deal_number": null, "created_at": "2026-09-27T10:00:00.000Z" },
    { "id": "cm2…", "content": "XXXX-YYYY-ZZZZ-0000", "is_sold": true, "deal_number": 10452, "created_at": "2026-09-26T08:12:00.000Z" }
  ],
  "meta": { "total": 43, "unsold": 42, "sold": 1, "limit": 500, "offset": 0 }
}
DELETE/api/v1/listings/{id}/stocklistings:write

Удаляет непроданные строки: {"ids": ["cm2…", "cm2…"]} или весь непроданный остаток {"all": true}. Проданные строки удалить нельзя — они остаются как история выдачи.

Загрузка картинок

POST /api/upload (вне /v1) принимает multipart/form-data с полями file и kind (listings, avatars или chat). Допустимы JPEG, PNG, WEBP и GIF до 8 МБ — формат проверяется по содержимому файла. Работает с API-ключом со scope listings:write. Полученный url передавайте в images объявления или во вложения чата.

curl -X POST https://45-88-14-102.sslip.io/api/upload -H "Authorization: Bearer $KEY" \
  -F "kind=listings" -F "file=@cover.webp"

{ "url": "/uploads/listings/9b1f0c2e4a7d6e3f1a2b3c4d.webp" }

Ошибка этого эндпоинта имеет вид {"error": "текст", "code": "invalid_file"}.

Сделки

Список и карточка сделки

GET/api/v1/deals?role=seller&status=PAID,DELIVERED&cursor=&limit=deals:read
GET/api/v1/deals/{number}deals:read

role — seller (по умолчанию, ваши продажи) или buyer. Сделка идентифицируется номером number, как на сайте.

Ответ GET /deals/10452
{
  "data": {
    "id": "cm3…", "number": 10452, "role": "seller", "status": "PAID",
    "listing": { "id": "cm1…", "short_id": "6992fc5806ba", "external_id": "sku-1002", "title": "2800 V-Bucks" },
    "buyer": { "id": "cm0…", "username": "player42" },
    "quantity": 2,
    "unit_price_kopecks": 129000, "unit_price_rub": "1290.00",
    "amount_kopecks": 258000, "amount_rub": "2580.00",
    "total_kopecks": 270900, "total_rub": "2709.00",
    "seller_fee_kopecks": 25800, "seller_fee_rub": "258.00",
    "seller_payout_kopecks": 232200, "seller_payout_rub": "2322.00",
    "with_guarantee": false, "delivered_content": null,
    "paid_at": "2026-09-27T10:05:00.000Z", "auto_complete_at": null
  }
}

Выдать товар

POST/api/v1/deals/{number}/deliverdeals:write

Для ручной выдачи: передайте данные для покупателя в content (до 5000 символов, необязательно) — они сохранятся в сделке, а сделка перейдёт в DELIVERED. Работает только для оплаченной сделки, где вы продавец.

curl -X POST https://45-88-14-102.sslip.io/api/v1/deals/10452/deliver -H "Authorization: Bearer $KEY" \
  -H "Content-Type: application/json" -d '{"content": "Код: AAAA-BBBB-CCCC"}'

Чат сделки

GET/api/v1/deals/{number}/messages?after=2026-09-27T10:00:00Z&limit=100deals:read
POST/api/v1/deals/{number}/messagesdeals:write

after — вернуть только сообщения новее указанного времени (удобно для опроса), mark_read=true — отметить прочитанными. Системные сообщения площадки имеют is_system: true и sender_id: null.

curl -X POST https://45-88-14-102.sslip.io/api/v1/deals/10452/messages -H "Authorization: Bearer $KEY" \
  -H "Content-Type: application/json" -d '{"text": "Здравствуйте! Выдаю товар в течение 5 минут."}'

Статусы сделок

СтатусЧто значитЧто делать продавцу
PENDING_PAYMENTСоздана, ждёт оплатыНичего — товар зарезервирован
PAIDОплачена, деньги у площадкиВыдать товар и вызвать /deliver (AUTO выдаётся сам)
DELIVEREDТовар передан, ждём подтвержденияЖдать; сделка закроется сама по таймеру
COMPLETEDЗавершенаВыплата зачислена в холд, затем на баланс
DISPUTEDОткрыт спорОтветить в чате, решение примет модератор
REFUNDEDДеньги возвращены покупателю—
CANCELLEDОтменена до оплаты—

Вебхуки

Добавьте адрес в кабинете и выберите события. При событии мы отправим POST с JSON-телом. Адрес — только https://. Ответьте кодом 2xx в течение 5 секунд; иначе будет ещё 3 попытки (через 2, 10 и 30 секунд). Ответ 4xx, кроме 408 и 429, считается окончательным отказом. Порядок доставки не гарантирован, возможны повторы — проверяйте data.status и храните обработанные X-Delivery-Id.

СобытиеКогда
deal.paidСделка оплачена — нужно выдать товар
deal.deliveredТовар отмечен выданным
deal.completedСделка завершена, выплата начислена
deal.disputedПокупатель открыл спор
deal.refundedСпор решён возвратом покупателю
deal.cancelledСделка отменена до оплаты
webhook.testКнопка «Отправить тест» в кабинете
ЗаголовокЗначение
X-EventИмя события
X-Signaturesha256=<hex HMAC-SHA256 от сырого тела с вашим секретом>
X-Delivery-IdId доставки, одинаковый у повторов одного события
X-Delivery-AttemptНомер попытки: 1–4
X-Webhook-IdId вебхука в кабинете
Тело запроса (deal.paid)
{
  "event": "deal.paid",
  "created_at": "2026-09-27T10:05:01.123Z",
  "data": {
    "dealId": "cm3…", "number": 10452, "listingId": "cm1…", "buyerId": "cm0…",
    "quantity": 2, "amount": 258000, "sellerPayout": 232200, "status": "PAID"
  }
}

Суммы в data — в копейках. Полную карточку сделки получите через GET /deals/{number}. Проверяйте подпись по сырому телу запроса до разбора JSON и сравнивайте в постоянное время.

Node.js (Express)
import express from "express";
import crypto from "crypto";

const SECRET = process.env.WEBHOOK_SECRET; // whsec_...
const app = express();

app.post("/hooks/market", express.raw({ type: "application/json" }), (req, res) => {
  const expected = "sha256=" + crypto.createHmac("sha256", SECRET).update(req.body).digest("hex");
  const got = req.get("X-Signature") || "";
  const ok = got.length === expected.length &&
    crypto.timingSafeEqual(Buffer.from(got), Buffer.from(expected));
  if (!ok) return res.status(401).end();

  const { event, data } = JSON.parse(req.body.toString("utf8"));
  if (event === "deal.paid") {
    // выдать товар: POST /api/v1/deals/{data.number}/deliver
  }
  res.status(200).end();
});

app.listen(3001);
Python (Flask)
import hmac, hashlib, os
from flask import Flask, request, abort

SECRET = os.environ["WEBHOOK_SECRET"].encode()
app = Flask(__name__)

@app.post("/hooks/market")
def hook():
    body = request.get_data()  # сырые байты
    expected = "sha256=" + hmac.new(SECRET, body, hashlib.sha256).hexdigest()
    if not hmac.compare_digest(expected, request.headers.get("X-Signature", "")):
        abort(401)
    event = request.get_json()
    if event["event"] == "deal.paid":
        pass  # выдать товар: POST /api/v1/deals/<number>/deliver
    return "", 200

OpenAPI

Машиночитаемая спецификация OpenAPI 3.1 — /api/v1/openapi.json. Её можно импортировать в Postman, Insomnia или сгенерировать по ней клиент.

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