API для реселлеров
Покупайте товары LH Лавки со своего баланса через HTTP и продавайте у себя: в своём боте, на сайте или в магазине. Цены те же, что в Маркете, товар выдаётся сразу в ответе API.
Быстрый старт
- Откройте бота, нажмите «API» → «Получить ключ». Ключ показывается один раз — сохраните его.
- Пополните баланс кнопкой «Пополнить». Все покупки через API списываются с него.
- Возьмите каталог
GET /productsи купите товарPOST /orders— товар придёт сразу в ответе.
Адрес API
…/api/v1Ответы — JSON в UTF-8, в каждом есть ok. Деньги — в копейках (*_cents) и в рублях. Время — UTC, ISO 8601.
OpenAPI-спецификация: openapi.json — импортируйте в Postman или Insomnia, или сгенерируйте клиент для своего языка.
Авторизация
Authorization: Bearer lh_ваш_ключ
Ключ даёт доступ к вашему балансу — храните его только на сервере. Если ключ утёк, перевыпустите его в боте: старый перестанет работать сразу, настройки сохранятся. Для защиты можно разрешить запросы только с ваших IP (настройки).
Лимит — 5 запросов в секунду (короткие всплески до 30). При превышении — ошибка rate_limited с полем retry_after в секундах.
Каталог
Товары, которые можно купить прямо сейчас. q — поиск по названию, brand — ключ бренда, all=1 — показать и то, чего нет в наличии (available: false). Обновляется каждые 15 секунд.
{"ok": true, "count": 1, "products": [
{"id": 17, "name": "Claude Pro — 1 месяц", "title": "Claude Pro — 1 месяц",
"brand": "claude", "brand_title": "Claude", "tag": "Ключ активации",
"price": 2199.0, "price_cents": 219900, "available": true, "stock": 8,
"delivery": "instant", "preorder_hours": null, "input": null}
]}
| Поле | Значение |
|---|---|
stock | Сколько штук в наличии; null — без ограничения. |
delivery | instant — выдача сразу; preorder — в течение preorder_hours часов; manual — выдаёт администратор. |
input | Если не null — товару нужны данные покупателя (например, ссылка на Discord-сервер). Передайте их в поле input при покупке; подсказка — в input.hint. |
Один товар, вместе с описанием (description).
Бренды, у которых сейчас есть товары: key, title, количество товаров. Удобно для меню в своём боте.
Покупка
{"product_id": 17, "idempotency_key": "order-10045", "quantity": 1, "max_price_cents": 219900}
| Поле | Описание |
|---|---|
product_id | ID товара. Обязательно. |
idempotency_key | Ваш уникальный ID покупки, до 64 символов, без #. Обязательно. Повтор с тем же ключом не спишет деньги второй раз, а вернёт те же заказы — смело повторяйте запрос после таймаута или обрыва связи. |
quantity | Сколько штук, от 1 до 10. По умолчанию 1. Каждая штука — отдельный заказ. |
max_price_cents | Цена за штуку, выше которой не покупать. Защита от изменения цены между каталогом и покупкой. |
input | Данные покупателя, если у товара input не null. |
dry_run | true — только проверить (товар, наличие, баланс, данные) и посчитать сумму, ничего не покупая. |
{"ok": true, "total_cents": 219900, "balance_cents": 130100,
"order": { …первый заказ… },
"orders": [{
"id": "3f2c…", "idempotency_key": "order-10045", "product_id": 17,
"title": "Claude Pro — 1 месяц", "price": 2199.0, "price_cents": 219900,
"state": "delivered", "created_at": "2026-11-12T09:30:00Z",
"delivered_at": "2026-11-12T09:30:02Z", "refunded_at": null,
"items": [{"type": "text", "text": "https://…"}, {"type": "text", "text": "Инструкция: …"}]
}]}
Перед покупкой проверяется, хватит ли денег и товара на всё количество — если нет, ничего не списывается. Если товар закончился посреди покупки нескольких штук, придут уже купленные заказы с "partial": true и причиной в error.
Для quantity больше 1 ключи штук — ваш_ключ, ваш_ключ#1, ваш_ключ#2…
Заказы
Заказ и выданный товар. Если заказ ещё в processing, передайте wait — ответ придёт, как только товар будет выдан (но не позже чем через указанное число секунд, максимум 25).
| state | Что значит |
|---|---|
| delivered | Товар выдан, он в items. |
| processing | Оплачено, получаем товар у поставщика — обычно секунды. |
| waiting | Предзаказ или ручная выдача — ждём администратора. |
| refunded | Возврат: деньги вернулись на баланс. |
Файлы в items приходят как {"type": "file", "download": "/orders/{id}/items/{n}"} — скачайте их тем же ключом.
Ваши покупки, новые первыми (до 100 за запрос). Найти заказ по своему ключу — ?idempotency_key=order-10045. Только покупки через API — ?source=api.
Баланс: balance, balance_cents.
Вебхуки
Чтобы не опрашивать заказы, задайте адрес — мы сами пришлём POST, когда заказ выдан, ушёл в ожидание или по нему сделан возврат:
PATCH /settings
{"webhook_url": "https://example.com/lh-webhook"}
В ответе — webhook_secret для проверки подписи. Тело события:
POST https://example.com/lh-webhook
X-LH-Event: order.delivered
X-LH-Timestamp: 1762939802
X-LH-Signature: sha256=5d41…
{"event": "order.delivered", "created_at": "2026-11-12T09:30:02Z", "order": { …как в GET /orders/{id}… }}
| Событие | Когда |
|---|---|
order.delivered | Товар выдан (в том числе сразу при покупке). |
order.waiting | Предзаказ или ручная выдача. |
order.refunded | Возврат на баланс. |
ping | Тест: POST /webhook/test. |
Подпись: HMAC-SHA256 от строки X-LH-Timestamp + "." + тело с ключом webhook_secret. Отвечайте кодом 2xx в течение 8 секунд. Если не получилось — повторим до 10 раз с растущей паузой (до часа). Одно событие может прийти дважды — сверяйте по order.id и state. Шлём только на публичные адреса.
import hmac, hashlib
def verify(secret: str, timestamp: str, body: bytes, signature: str) -> bool:
expected = hmac.new(secret.encode(), timestamp.encode() + b"." + body, hashlib.sha256).hexdigest()
return hmac.compare_digest("sha256=" + expected, signature)
Настройки
Передайте только поля, которые меняете:
| Поле | Описание |
|---|---|
webhook_url | Адрес для вебхуков; null — выключить. |
chat_copies | true — дублировать каждую API-покупку сообщением в чат с ботом. По умолчанию выключено (то же переключается в боте). |
ip_allowlist | Список IP или подсетей, с которых разрешены запросы, например ["203.0.113.5", "198.51.100.0/24"]. Пустой список — с любых. |
low_balance_alert_cents | Когда баланс опустится ниже этой суммы, бот пришлёт предупреждение. null — выключить. |
Перевыпустить секрет подписи.
Ошибки
При ошибке ok = false, в error — код, текст на русском и иногда подробности (например, stock или required_cents). Если покупка вернула ошибку — деньги не списаны.
{"ok": false, "error": {"code": "insufficient_balance", "message": "Недостаточно средств: нужно 2199.0 ₽, на балансе 1500.0 ₽.",
"balance_cents": 150000, "required_cents": 219900}}
| HTTP | code | Когда |
|---|---|---|
| 400 | bad_request, input_required, bad_input, bad_webhook_url | Неверные параметры. |
| 401 | unauthorized | Нет ключа, ключ неверный или перевыпущен. |
| 402 | insufficient_balance | Не хватает денег на всё количество. |
| 403 | ip_not_allowed | Запрос не с разрешённого IP. |
| 404 | not_found | Нет такого товара, заказа или метода. |
| 409 | product_unavailable, out_of_stock, price_changed, purchase_failed | Купить сейчас нельзя. |
| 429 | rate_limited | Больше 5 запросов в секунду. |
| 5xx | internal и др. | Сбой у нас. Повторите запрос с тем же idempotency_key. |
Примеры
curl
curl -s BASE/products?q=claude -H "Authorization: Bearer $LH_KEY"
curl -s -X POST BASE/orders \
-H "Authorization: Bearer $LH_KEY" -H "Content-Type: application/json" \
-d '{"product_id": 17, "idempotency_key": "order-10045"}'
Python
import requests
BASE = "BASE"
S = requests.Session()
S.headers["Authorization"] = "Bearer lh_ваш_ключ"
def buy(product_id: int, my_order_id: str, quantity: int = 1) -> list[dict]:
"""my_order_id — ID заказа в вашей системе: повтор не спишет деньги второй раз."""
for attempt in range(3):
try:
r = S.post(f"{BASE}/orders", timeout=60, json={
"product_id": product_id, "idempotency_key": my_order_id, "quantity": quantity}).json()
break
except requests.RequestException:
continue # сеть — повторяем с тем же ключом
else:
raise RuntimeError("API недоступно")
if not r["ok"]:
raise RuntimeError(r["error"]["message"])
orders = r["orders"]
for i, order in enumerate(orders): # редкий случай: товар ещё в пути у поставщика
while order["state"] == "processing":
order = S.get(f"{BASE}/orders/{order['id']}", params={"wait": 25}, timeout=40).json()["order"]
orders[i] = order
return orders
for order in buy(17, "my-order-10045"):
print(order["state"], [item.get("text") for item in order["items"]])
Node.js
const BASE = "BASE";
const headers = { Authorization: `Bearer ${process.env.LH_KEY}`, "Content-Type": "application/json" };
const res = await fetch(`${BASE}/orders`, { method: "POST", headers,
body: JSON.stringify({ product_id: 17, idempotency_key: "my-order-10045" }) });
const data = await res.json();
if (!data.ok) throw new Error(data.error.message);
console.log(data.order.state, data.order.items);
PHP
$ch = curl_init("BASE/orders");
curl_setopt_array($ch, [CURLOPT_POST => true, CURLOPT_RETURNTRANSFER => true, CURLOPT_TIMEOUT => 60,
CURLOPT_HTTPHEADER => ["Authorization: Bearer " . getenv("LH_KEY"), "Content-Type: application/json"],
CURLOPT_POSTFIELDS => json_encode(["product_id" => 17, "idempotency_key" => "my-order-10045"])]);
$data = json_decode(curl_exec($ch), true);
if (!$data["ok"]) throw new Exception($data["error"]["message"]);
print_r($data["order"]["items"]);