REST API v1

Seller API

Connect your inventory system, bot or scraper to Lootmarket: upload and sync listings in bulk, top up the auto-delivery stock, change prices, receive paid deals via webhooks and deliver goods automatically.

Base URL: https://45-88-14-102.sslip.io/api/v1

Quick start

  1. Create a key in your account → API and webhooks. The key is shown only once.
  2. Find a category for your goods with GET /games and its fields with GET /games/{slug}.
  3. Upload listings in one POST /listings/bulk request with your external_id values and repeat it on every sync.
  4. For auto-delivery, top up the stock with POST /listings/{id}/stock; for manual delivery, subscribe to the deal.paid webhook.
Check your key
curl https://45-88-14-102.sslip.io/api/v1/me \
  -H "Authorization: Bearer mk_your_key"

Authentication

Every request is signed with an API key of the form mk_…. Pass it in the Authorization: Bearer <key> or X-Api-Key: <key> header. We store only a hash of the key; your account shows its prefix, creation date and last use. Revoke a compromised key in your account — it stops working immediately.

A key has permissions (scopes). A request without the required scope gets 403.

ScopeWhat it allows
listings:readEʼlonlar va omborni oʻqish
listings:writeEʼlonlar, narxlar va omborni yaratish va oʻzgartirish
deals:readBitimlar va chatlarni oʻqish
deals:writeMahsulotni topshirish va bitim chatlarida xabarlar

GET /me, GET /games and GET /games/{slug} are available to any valid key.

Responses and errors

All responses are UTF-8 JSON. A successful response contains data and, for lists and batch operations, meta. Fields are in snake_case, dates are ISO 8601 in UTC.

Success
{
  "data": { "id": "cm1…", "title": "1000 V-Bucks", "price_kopecks": 49900, "price_rub": "499.00" },
  "meta": { "limit": 50, "has_more": false, "next_cursor": null }
}
Error
{
  "error": {
    "code": "validation_error",
    "message": "price_kopecks: Минимальная цена — 1 ₽",
    "details": [{ "path": "price_kopecks", "message": "Минимальная цена — 1 ₽" }]
  }
}

The message texts are human-readable and currently in Russian. Branch your code on code and details[].path, not on the message text.

HTTPcodeWhen
400bad_request, invalid_jsonInvalid parameters or the request body is not JSON
401unauthorizedNo key, or the key is invalid or revoked
403forbiddenThe key lacks the required scope, or the action is forbidden
404not_foundThe object does not exist or belongs to another user
409conflictexternal_id is already used by another listing
422validation_error, unprocessableValidation failed, or the action is not allowed in the current status
429rate_limitedRate limit exceeded
500internal_errorError on our side — retry later

Limits and pagination

No more than 120 requests per minute per key. Every response carries the X-RateLimit-Limit, X-RateLimit-Remaining and X-RateLimit-Reset (unix time when the window resets) headers. Over the limit you get 429 and Retry-After in seconds. Use the batch endpoints for bulk work: one bulk call with 500 listings counts as a single request.

Lists are paginated: limit from 1 to 100 (default 50) and cursor. Request the next page with cursor=meta.next_cursor until meta.has_more becomes false.

curl "https://45-88-14-102.sslip.io/api/v1/listings?status=ACTIVE&limit=100&cursor=cm1abc…" -H "Authorization: Bearer $KEY"
OperationPer-request limit
POST /listings/bulk500 listings
PATCH /listings/prices1000 items
POST /listings/{id}/stock5000 lines, up to 2000 characters each
POST /api/uploadfile up to 8 MB, 30 uploads per minute

Money

All amounts are in Russian rubles, as integer kopecks (1 ruble = 100 kopecks) in *_kopecks fields. For convenience a ruble string *_rub with two decimal places is duplicated next to it. When writing you may pass either: price_kopecks: 49900 or price_rub: "499.00" (also "499,00" and the number 499). If both are passed, price_kopecks wins.

Profile and catalog

Profile and balance

GET/api/v1/me
Response
{
  "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"] }
  }
}

Game list

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

type is GAME, MOBILE_GAME or APPLICATION; q searches by name. Each game in the response has a list of categories with an id — you need it to create listings.

Game with category fields

GET/api/v1/games/{slug}

Returns categories and their fields (fields). Field values go into the listing's attributes object under the field's key. For SELECT the value must match one of the options, for NUMBER it is a number, and fields with required: true are mandatory.

Response
{
  "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 }
      ]
    }]
  }
}

Listings

A listing belongs to the key owner; other sellers' listings do not exist for the API (404). If the marketplace has moderation enabled, a new listing gets the PENDING status, and changing the price, title or category of an active listing sends it back for review.

FieldTypeDescription
category_idstringCategory from /games — required on create
titlestring3–120 characters
descriptionstringup to 5000 characters
price_kopecks | price_rubint | stringPrice per unit, from 1 ₽ to 1,000,000 ₽
old_price_kopecksint | nullStrikethrough price, must be higher than the current one
quantityintQuantity for MANUAL; for AUTO it is counted from the stock
delivery_typeMANUAL | AUTOManual delivery in chat or auto-delivery from the stock
delivery_infostringInstructions for the buyer after payment
attributesobjectCategory field values {key: value}
imagesstring[]Up to 10 URLs: from POST /api/upload or external https
external_idstringYour item id, unique per seller
stockstring[]AUTO only: lines to add to the stock
buyer_fieldsobject[] | nullUp to 5 fields the buyer fills in at checkout (player ID, server): {key, label, type: text|number|email|select, required, options, placeholder, help}. Omitted — unchanged; null or [] — no fields
variantsobject[] | nullUp to 30 product variants with their own price and quantity (see “Product variants”). Omitted — unchanged; null or [] — no variants
rental_unitHOUR | DAY | nullAccount rental — only in categories with rental_allowed: the price is per hour or per day, quantity is how many accounts are rented out at once; MANUAL only, no variants. null — a regular sale
rental_min_units | rental_max_unitsint | nullRental: order term in hours or days (at most 720 hours or 90 days). Omitted — unchanged

List listings

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

status and external_id accept several comma-separated values.

Create a listing

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 to your account",
    "description": "Top-up via gift code, 5 minutes.",
    "price_rub": "499.00",
    "quantity": 50,
    "delivery_type": "MANUAL",
    "attributes": { "platform": "PC" },
    "images": ["/uploads/listings/3f9c….webp"]
  }'
Response 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-to-your-account",
    "status": "ACTIVE", "price_kopecks": 49900, "price_rub": "499.00",
    "old_price_kopecks": null, "old_price_rub": null, "quantity": 50, "delivery_type": "MANUAL", …
  }
}

Get, update, delete

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

You can pass short_id instead of id. PATCH changes only the fields you pass. DELETE removes the listing; if it already has deals, it is archived instead (archived: true), and with deals still in progress it returns 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}'

Change status

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

{"status": "PAUSED"} hides the listing from the storefront, ACTIVE brings it back, ARCHIVED archives it. Only an active listing can be paused; a listing under moderation or a rejected one cannot be activated (422).

Bulk upload and sync (bulk upsert)

POST/api/v1/listings/bulklistings:write

Up to 500 listings per request. external_id is required — it is the idempotency key: if you already have a listing with that external_id, it is updated (the fields you pass override the current ones); otherwise a new one is created. Repeating the same request creates no duplicates, so you can run the sync on a schedule. An error in one item does not cancel the others — you get a result for each. The optional status field (ACTIVE | PAUSED | ARCHIVED) lets you take items you no longer have off sale in the same call.

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": "Account with 50 skins",
        "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" }
    ]
  }'
Response 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": "PENDING",
      "quality": [{ "code": "contacts", "level": "block", "field": "description", "message": "В описании есть ссылка. Контакты и ссылки запрещены: общение и оплата — только через сделку на площадке" }] },
    { "index": 2, "external_id": "sku-0999", "result": "error",
      "error": { "code": "unprocessable", "message": "Выберите категорию" } }
  ],
  "meta": { "total": 3, "created": 1, "updated": 1, "errors": 1 }
}

To update an existing listing, external_id and the changed fields are enough. To only archive a listing, use POST /listings/{id}/status or pass its full data together with status. On update, lines from stock are added to the stock (duplicates are skipped) rather than replacing it.

Bulk price update

PATCH/api/v1/listings/priceslistings:write

Up to 1000 items. A listing is identified by id or external_id. If the new price is not lower than the old strikethrough price and old_price_kopecks is not passed, the strikethrough price is removed.

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}
      ]}'
Response
{
  "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 }
}

Product variants

One product — several variants with their own price and quantity: “Region: RU / EU / Global”, “80 / 400 / 800 Robux”. The buyer picks a variant on the product page, and the deal uses the variant's price and stock. The price of a listing with variants (price_kopecks, has_variants: true) is the cheapest active variant, shown as “from X ₽”; quantity is the sum of active variants' stock. Variant parameters (attributes) are category field keys from GET /games/{slug} (the value must be one of their options) or your own names, up to 3 parameters. For AUTO listings each variant has its own code stock: stock in the variant or POST …/stock with variant_id.

A listing with variants
curl -X POST https://45-88-14-102.sslip.io/api/v1/listings -H "Authorization: Bearer $KEY" \
  -H "Content-Type: application/json" \
  -d '{"category_id": "cm0…", "title": "Робуксы, быстрая выдача", "delivery_type": "MANUAL",
       "variants": [
         {"attributes": {"Количество": "400"}, "price_rub": "399", "quantity": 50},
         {"attributes": {"Количество": "800"}, "price_rub": "749", "quantity": 20},
         {"title": "1700 робуксов", "price_kopecks": 149000, "old_price_kopecks": 169000, "quantity": 5}
       ]}'

In POST/PATCH /listings and bulk the variants list is complete: a variant with an id is updated, one without an id is created, a missing one is deleted (archived if it has deals). A variant's quantity changes only when passed; stock adds codes. For single changes use:

GET/api/v1/listings/{id}/variantslistings:read
POST/api/v1/listings/{id}/variantslistings:write
PATCH/api/v1/listings/{id}/variants/{variantId}listings:write
DELETE/api/v1/listings/{id}/variants/{variantId}listings:write
Response: a variant
{
  "data": {
    "id": "cm3…", "title": "400", "attributes": { "Количество": "400" },
    "price_kopecks": 39900, "price_rub": "399.00", "old_price_kopecks": null, "old_price_rub": null,
    "quantity": 50, "sort_order": 0, "is_active": true
  }
}

A variant's price can also be changed in PATCH /listings/prices with a variant_id field. In deals (GET /deals, webhooks) the variant comes in variant / variantId, and the deal's unit_price is the variant's price.

Quality check: the quality field

Every created or updated listing goes through an automatic check: contacts and links that take the deal off the platform, banned words, an all-caps or spammy title, a price far from the market, duplicates, an account without a picture, auto-delivery without stock. If the check finds problems, the responses of POST /listings, GET and PATCH /listings/{id}, POST …/status, POST …/stock, and every item of bulk and prices carry a quality array. No problems — no field.

FieldTypeDescription
codestringRule code: contacts, banned_words, price_low, duplicate…
levelblock | warnblock — the listing is not published; warn — it is published, but a moderator will review it
messagestringReason for the seller (in Russian)
fieldstring?Listing field: title, description, price, images, category, stock

With a block issue the listing gets status: "PENDING", and reject_reason holds the reason prefixed with «Автопроверка: ». Fix the listing and send it again (PATCH or bulk): once no problems remain, it goes back on sale by itself. The exception is auto-delivery without stock: such a listing gets SOLD and becomes active after a stock top-up. warn issues do not stop publication.

Response fragment
{
  "data": {
    "id": "cm1…", "status": "PENDING",
    "reject_reason": "Автопроверка: В описании есть ссылка. Контакты и ссылки запрещены: общение и оплата — только через сделку на площадке",
    …,
    "quality": [
      { "code": "contacts", "level": "block", "field": "description", "message": "В описании есть ссылка. Контакты и ссылки запрещены: общение и оплата — только через сделку на площадке" },
      { "code": "caps_title", "level": "warn", "field": "title", "message": "Название набрано заглавными буквами" }
    ]
  }
}

Auto-delivery stock

For listings with delivery_type: "AUTO" the goods are delivered to the buyer right after payment — one stock line per unit (a code, login:password, a link). The quantity of such a listing always equals the number of unsold lines. Empty lines and duplicates (in the request and in the stock) are skipped. If the listing was sold out, topping up the stock makes it active again.

POST/api/v1/listings/{id}/stocklistings:write
Add codes
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"]}'
Response 201
{ "data": { "added": 3, "skipped": 0, "in_stock": 42, "status": "ACTIVE" } }
GET/api/v1/listings/{id}/stock?sold=false&limit=500&offset=0listings:read
Response
{
  "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

Deletes unsold lines: {"ids": ["cm2…", "cm2…"]} or the whole unsold stock {"all": true}. Sold lines cannot be deleted — they remain as the delivery history.

Image upload

POST /api/upload (outside /v1) accepts multipart/form-data with the file and kind fields (listings, avatars, shop — the shop banner, or chat). JPEG, PNG, WEBP and GIF up to 8 MB are allowed — the format is checked by the file contents. Works with an API key that has the listings:write scope. Pass the returned url to the listing's images or to chat attachments.

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" }

Errors from this endpoint look like {"error": "text", "code": "invalid_file"}.

Deals

Deal list and deal card

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

role is seller (default, your sales) or buyer. A deal is identified by its number, the same as on the website.

buyer_input is what the buyer entered at checkout for the listing's buyer_fields (player ID, server): an array of {key, label, value}, empty if there were no fields.

Response 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,
    "buyer_input": [{ "key": "player_id", "label": "ID игрока", "value": "812345678" }],
    "delivered_content": null,
    "paid_at": "2026-09-27T10:05:00.000Z", "auto_complete_at": null
  }
}

Deliver goods

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

For manual delivery: pass the data for the buyer in content (up to 5000 characters, optional) — it is saved in the deal, and the deal moves to DELIVERED. Works only for a paid deal where you are the seller.

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": "Code: AAAA-BBBB-CCCC"}'

Deal chat

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

after returns only messages newer than the given time (handy for polling), mark_read=true marks them as read. Marketplace system messages have is_system: true and 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": "Hello! Delivering your order within 5 minutes."}'

Deal statuses

StatusMeaningWhat the seller does
PENDING_PAYMENTCreated, awaiting paymentNothing — the item is reserved
PAIDPaid, the marketplace holds the moneyDeliver the goods and call /deliver (AUTO delivers itself)
DELIVEREDDelivered, awaiting confirmationWait; the deal closes automatically on a timer
COMPLETEDCompletedPayout goes to hold, then to the balance
DISPUTEDDispute openedReply in the chat; a moderator decides
REFUNDEDMoney refunded to the buyer—
CANCELLEDCancelled before payment—

Webhooks

Add an endpoint in your account and choose the events. When an event happens we send a POST with a JSON body. Only https:// URLs are accepted. Reply with a 2xx code within 5 seconds; otherwise there will be 3 more attempts (after 2, 10 and 30 seconds). A 4xx response other than 408 and 429 is treated as a final rejection. Delivery order is not guaranteed and repeats are possible — check data.status and store the X-Delivery-Id values you have processed.

EventWhen
deal.paidBitim toʻlandi — mahsulotni topshirish kerak
deal.deliveredMahsulot topshirilgan deb belgilandi
deal.completedBitim yakunlandi, toʻlov hisoblandi
deal.disputedXaridor nizo ochdi
deal.refundedNizo xaridorga pulni qaytarish bilan hal qilindi
deal.cancelledBitim toʻlovdan oldin bekor qilindi
webhook.testThe “Send test” button in your account
HeaderValue
X-EventEvent name
X-Signaturesha256=<hex HMAC-SHA256 of the raw body with your secret>
X-Delivery-IdDelivery id, the same across retries of one event
X-Delivery-AttemptAttempt number: 1–4 (a manual retry from the log continues the count)
X-Webhook-IdWebhook id in your account
Request body (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",
    "buyerInput": [{ "key": "player_id", "label": "ID игрока", "value": "812345678" }]
  }
}

Amounts in data are in kopecks. buyerInput holds the buyer's checkout data (player ID, server), [] if there were no fields. Get the full deal card with GET /deals/{number}. Verify the signature against the raw request body before parsing the JSON, and compare in constant time.

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") {
    // deliver the goods: 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()  # raw bytes
    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  # deliver the goods: POST /api/v1/deals/<number>/deliver
    return "", 200

SDKs and examples

Ready-made API clients for JavaScript/TypeScript and Python: types generated from OpenAPI, retries after 429 that honour Retry-After, iterators over all pages, batches of any size (split to the API limits) and webhook signature verification. The archives contain the sources, a ready build, examples and tests.

FeatureJavaScript / TypeScriptPython
Dependenciesnone (fetch, WebCrypto)requests
RuntimeNode 18+, Deno, Bun, WorkersPython 3.9+
Typesfrom OpenAPI (openapi-typescript)TypedDict from OpenAPI, py.typed
Rate limit 429waits Retry-After and retrieswaits Retry-After and retries
Paginationfor await … iterate()for … in iter()
Webhook signatureconstructWebhookEvent()construct_webhook_event()

JavaScript / TypeScript

Install: unpack the archive and add the folder
npm install ./marketplace-sdk-js
index.mjs
import { MarketplaceClient, constructWebhookEvent } from "marketplace-seller-sdk";

const client = new MarketplaceClient({ apiKey: process.env.MARKET_API_KEY, baseUrl: "https://45-88-14-102.sslip.io" });

// bulk upload by external_id: new ones are created, known ones updated
const { meta } = await client.listings.bulkUpsert(items);          // any number — batches of 500
await client.listings.updatePrices([{ external_id: "sku-1", price_rub: "449.00" }]);

for await (const deal of client.deals.iterate({ status: "PAID" })) {
  await client.deals.deliver(deal.number, "KEY-XXXX");
}

// webhook: raw body, request headers and the secret
const { event, deliveryId } = await constructWebhookEvent(rawBody, req.headers, process.env.WEBHOOK_SECRET);

Python

Install
pip install ./marketplace-sdk-python
main.py
from marketplace_sdk import MarketplaceClient, construct_webhook_event

client = MarketplaceClient(os.environ["MARKET_API_KEY"], "https://45-88-14-102.sslip.io")

results, meta = client.listings.bulk_upsert(items)   # batches of 500
client.listings.update_prices([{"external_id": "sku-1", "price_rub": "449.00"}])

for deal in client.deals.iter(status="PAID"):
    client.deals.deliver(deal["number"], "KEY-XXXX")

hook = construct_webhook_event(request.get_data(), request.headers, WEBHOOK_SECRET)  # Flask

Examples in the archives

ExampleWhat it does
import-csv · import_csv.pyBulk catalog upload from CSV by external_id (template catalog.example.csv: prices, quantity, stock codes separated by “|”, category fields attr:<key>). Safe to re-run; rows with errors go to a separate CSV.
sync-prices-stock · sync_prices_stock.pyPrice and stock sync from your feed: sends only the changes, adds new auto-delivery codes, and with --pause-missing pauses what is missing from the feed. Meant to run on a schedule.
webhook-autodelivery · webhook_autodelivery.pyWebhook receiver: signature check, duplicate protection by X-Delivery-Id, and on deal.paid a code from your pool plus delivery. Test events from the tester are acknowledged but not processed.

You can test your handler without real purchases in your account: the “Webhook tester” sends a sample of any event to your URL and shows the response code and time, and the “Delivery log” shows every attempt for 7 days — a failed one can be sent again with the “Retry” button (same X-Delivery-Id).

Postman / Insomnia

Import postman_collection.json (Postman: Import; Insomnia: Import → From File — Postman v2.1 format, or openapi.json itself). Set the collection variables baseUrl = https://45-88-14-102.sslip.io/api/v1, siteUrl = https://45-88-14-102.sslip.io and apiKey = your key. Requests are grouped by section, and the ones that change data come with an example body.

OpenAPI

The machine-readable OpenAPI 3.1 specification is at /api/v1/openapi.json. You can import it into Postman or Insomnia, or generate a client from it. Its descriptions are in Russian.