# SupplyMe · Client API

Версия контракта 1.5.0 · 12 октября 2026

## Подключение

API предназначен для серверной интеграции: каталог, баланс партнёра, закупка товаров и получение выданных данных. Доступ выдаётся на функции магазина. Административных прав клиентский токен не даёт.

Базовый адрес: `https://supply-me.ru/api/v1`. Все методы ниже указываются относительно него. Документация доступна без ключа: [страница API](/api/docs), [OpenAPI 3.0 JSON](/api/v1/openapi.json), [скачать Markdown](/api/docs.md).

Войдите в SupplyMe через Telegram и откройте «Профиль → API». Создайте ключ для своей интеграции. Обычный ключ разрешает чтение каталога, своего баланса и заказов этого API-клиента (`shop_read`). Для автоматических закупок отдельно разрешите покупки с баланса (`shop_purchase`): каждый новый успешный `POST /orders` списывает реальные средства вашего аккаунта.

Можно иметь до пяти активных ключей. Полный ключ показывается один раз при создании. Скопируйте его и храните на backend в переменной окружения или хранилище секретов; не помещайте ключ в браузерный код, публичный репозиторий или URL. В профиле можно посмотреть ограничения, использование и отозвать ключ. Создание и отзыв выполняются в авторизованном профиле: Bearer-ключ не даёт доступа к управлению ключами, профилем или административным методам.

При отзыве ключ перестаёт проходить новые проверки авторизации; уже завершённые покупки сохраняются в истории аккаунта. Новый ключ, созданный в профиле, относится к новому API-клиенту: его история и `Idempotency-Key` независимы от прежнего. До отзыва ключа восстановите результат всех покупок с неопределённым исходом. Не повторяйте такую покупку с новым API-клиентом — это может создать второй заказ и повторное списание. При утрате ключа проверьте историю покупок в профиле и обратитесь в поддержку.

Для ключей, настроенных администратором, могут действовать срок окончания и список разрешённых IP. В списке используются публичные исходящие IP вашего backend, а не IP покупателя. Административный перевыпуск токена существующего API-клиента сохраняет его заказы и область идемпотентности, но немедленно отключает старый токен.

```http
Authorization: Bearer YOUR_API_KEY
Content-Type: application/json
Accept-Language: ru
X-Request-ID: your-order-123-check-1
```

`Authorization` обязателен для методов магазина. `Content-Type` обязателен для POST; поддерживается также тип с окончанием `+json`. Тело — UTF-8 JSON-объект. Предпочтительный язык: `ru` или `en`, по умолчанию `ru`. Приоритет: `?lang=`, затем `?language=`, `X-Language`, `Accept-Language`. Не передавайте секреты в URL.

`X-Request-ID` необязателен: 8–64 символа, латинские буквы, цифры, `.`, `_`, `:`, `-`. Сервер возвращает принятый ID в JSON и заголовке; неверный или отсутствующий ID заменяется новым. Это идентификатор диагностики. Для покупки `POST /orders` отдельно обязателен `Idempotency-Key`, защищающий от повторного списания.


## Перепродажа: подключение партнёра

Партнёр принимает оплату в своём магазине. После подтверждённой оплаты его backend покупает товар в SupplyMe с заранее пополненного баланса и получает данные для выдачи своему покупателю. SupplyMe не обращается к платёжной системе партнёра и не проверяет оплату его покупателя: эту проверку выполняет интеграция партнёра.

Создайте ключ в «Профиль → API» и включите покупки с баланса. Ключ автоматически привязан к вашему аккаунту; выбирать чужой аккаунт нельзя. Примите правила магазина и заранее пополните баланс через профиль или бота. Привязанный аккаунт впоследствии не заменяется: другому владельцу нужен отдельный API-клиент. Если для интеграции нужны особые ограничения или административный перевыпуск существующего токена, обратитесь в поддержку.

Покупки идут по текущей розничной цене товара. Автоматическая выдача поддерживает `text` и `link`, количество от 1 до 10 за запрос. Ручная оптовая выдача, Telegram-файлы и промокоды в этом методе не поддерживаются. Партнёр назначает свою цену покупателю у себя. Пополнение баланса выполняется через существующий профиль/бот магазина. Не выдавайте покупателю API-токен партнёра.

| Метод | Разрешение | Назначение |
| --- | --- | --- |
| `GET /status` | `shop_read` или разрешение старого ключа `check` | Состояние API, права и ограничения текущего ключа |
| `GET /catalog` | `shop_read` | Товары, цены, свободные остатки |
| `GET /balance` | `shop_read` | Текущий баланс партнёра |
| `POST /orders` | `shop_purchase` и включённый `shop_read` | Покупка и автоматическая выдача |
| `GET /orders/{order_id}` | `shop_read` | Повторное получение заказа и выдачи |
| `GET /orders` | `shop_read` | История заказов этого API-клиента |

Все суммы — целые сотые USDT: `1234` означает `12.34 USDT`. API не списывает рубли или показанную посетителю цену перепродажи. Не используйте floating point для расчёта `expected_total_usdt_cents`: умножьте целое значение цены из каталога на количество.

## Проверка подключения и ограничения

Начните с `GET /status`: этот метод не покупает товар и не списывает баланс. Он возвращает сведения о текущем API-ключе. Это состояние API SupplyMe; оно не подтверждает работоспособность сторонних сервисов или наличие нужного товара.

```bash
curl 'https://supply-me.ru/api/v1/status' \
  -H "Authorization: Bearer $SUPPLYME_API_KEY"
```

```json
{"success":true,"request_id":"req_status_demo","code":"service.status","message":"API SupplyMe работает","data":{"status":"normal","api":{"version":"v1","status":"operational","currency":"USDT"},"permissions":{"shop_read":true,"shop_purchase":false},"limits":{"rate_limit_per_minute":60,"daily_limit":10000},"account":{"bound":true}}}
```

Значения лимитов в примере иллюстративные; используйте значения своего ключа. Лимиты действуют суммарно на методы текущего API-клиента. Суточный счётчик сбрасывается по границе суток UTC. Запрос, прошедший авторизацию и принятый в пределах лимита, расходует квоту, включая запрос с ошибочными параметрами, неуспешную покупку и идемпотентный повтор. `429 rate.minute_exceeded` и `429 rate.daily_exceeded` содержат заголовок `Retry-After` в секундах. Дополнительно действует ограничение частоты по исходящему IP.

Во время технических работ `GET /status` возвращает `503 shop.maintenance`, `data.status=maintenance` и `data.api.status=maintenance`; новые покупки также могут возвращать `503 shop.maintenance`. Каталог, баланс, история и повтор ранее сохранённой покупки остаются доступны при действующем ключе. Сохраняйте `Idempotency-Key` и исходное тело покупки для восстановления результата.

Проверка и активация сторонних CDK, массовая проверка, обновление Claude и ChatGPT через старые методы не предоставляются в SupplyMe. Пути `/keys/check`, `/keys/bulk-check`, `/activate`, `/claude/refresh` и `/chatgpt/subscription` возвращают `410 feature_retired`; ключ из профиля не включает эти разрешения. Для закупки используйте методы магазина ниже.

## Каталог партнёра и баланс

`GET /catalog?limit=100&after_id=0` возвращает товары по возрастанию ID. `limit` от 1 до 100; `after_id` — ID последнего полученного товара, по умолчанию 0. Повторяйте с `next_after_id`, пока он не станет `null`. Товары без остатка могут присутствовать с `available_quantity=0`. Скрытые товары/категории и товары без автоматической выдачи не включаются.

```json
{"success":true,"request_id":"req_catalog_demo","code":"shop.catalog","message":"Каталог получен","data":{"products":[{"id":42,"title":"Пример подписки","description":"Условия и срок подписки","category_id":3,"price_usdt_cents":1250,"currency":"USDT","delivery_type":"text","available_quantity":25,"max_quantity_per_order":10}],"next_after_id":null}}
```

Цена и остаток актуальны на момент чтения, но чтение их не резервирует. Между оплатой покупателем у партнёра и API-покупкой товар может закончиться. Интеграция должна предусмотреть уведомление оператора, ожидание пополнения или возврат в своей платёжной системе; возврат нельзя автоматически делать при неопределённом результате API-покупки.

`GET /balance` возвращает текущий остаток только привязанного аккаунта; параметры чужого пользователя не предусмотрены.

```json
{"success":true,"request_id":"req_balance_demo","code":"shop.balance","message":"Баланс получен","data":{"balance_usdt_cents":10000,"currency":"USDT"}}
```

## Покупка и получение ключей

`POST /orders`. Обязательны Bearer-токен, `Content-Type: application/json`, `Idempotency-Key` и JSON-тело до 8192 байт. Неизвестные поля отклоняются. Строки и дробные числа вместо целых недопустимы.

| Поле | Тип | Описание |
| --- | --- | --- |
| `product_id` | integer, > 0, обязательно | ID из `/catalog` |
| `quantity` | integer, 1–10 | По умолчанию 1 |
| `expected_total_usdt_cents` | integer, 1–1000000000000, обязательно | Ожидаемая цена за единицу × количество |

`Idempotency-Key` — от 8 до 128 символов: латинские буквы, цифры, `_`, `.`, `:`, `-`. Сохраните ключ вместе с параметрами в своей БД до первого запроса. Удобно использовать стабильный ID оплаченного заказа, например `myshop-order-8457`. Один ключ относится к одной покупке. Использование другого API-клиента или нового ключа создаёт независимую покупку, даже если продукт тот же.

```bash
curl --connect-timeout 10 --max-time 30 \
  'https://supply-me.ru/api/v1/orders' \
  -H "Authorization: Bearer $SUPPLYME_API_KEY" \
  -H 'Content-Type: application/json' \
  -H 'Idempotency-Key: myshop-order-8457' \
  --data '{"product_id":42,"quantity":1,"expected_total_usdt_cents":1250}'
```

```json
{"success":true,"request_id":"req_purchase_demo","code":"shop.order","message":"Заказ получен","data":{"replayed":false,"order":{"id":1901,"idempotency_key":"myshop-order-8457","product_id":42,"product_title":"Пример подписки","quantity":1,"status":"delivered","total_usdt_cents":1250,"currency":"USDT","balance_after_usdt_cents":8750,"delivery":{"type":"text","content":"EXAMPLE-KEY-NOT-REAL","items":["EXAMPLE-KEY-NOT-REAL"],"post_purchase_info":"Инструкция по активации"},"created_at":"2026-09-15T00:00:00+00:00"}}}
```

HTTP `201` означает новый оплаченный и выданный заказ. Повтор с тем же ключом и параметрами возвращает `200`, `data.replayed=true` и прежний заказ. Списание, заказ, товарные остатки и квитанция повтора сохраняются одной транзакцией. После обрыва ответа или перезапуска сервера повторная покупка не создаётся.

Выдавайте товар своему покупателю только при `data.order.status=delivered` и `delivery != null`. `delivery.items` содержит отдельные выданные единицы с сохранением переносов строк; не разбивайте `content` по строкам, поскольку одна единица может включать логин, пароль и инструкцию. `content` — полная выдача заказа. `post_purchase_info` — дополнительная инструкция. Если поддержка вручную заменила содержимое заказа, `items` может быть `null`; актуальная выдача находится в `content`. Отображайте текст безопасно, без исполнения HTML/JavaScript из содержимого товара.

`idempotency_key` в заказе позволяет сопоставить запись из истории с заказом вашего магазина. `balance_after_usdt_cents` — остаток сразу после этой покупки; повтор возвращает то же значение. Для текущего баланса используйте `/balance`. Баланс и выдача также видны владельцу в обычной истории покупок SupplyMe.

## Повторы, ошибки покупки и история

| Ситуация | Действие интеграции |
| --- | --- |
| Тайм-аут, разрыв, не-JSON, HTTP 502/503/504 | Результат может быть неопределённым. Повторяйте тот же POST с тем же ключом и телом, с паузой. Не создавайте новый ключ и не объявляйте покупку неоплаченной |
| `429` | Соблюдайте `Retry-After`, сохраняйте ключ и тело |
| `shop.idempotency_conflict`, `409` | С этим ключом ранее отправлено другое тело. Восстановите исходные параметры; не обходите конфликт новым ключом без проверки прежней покупки |
| `shop.price_changed`, `409` | Цена не совпала, списания нет; `data.current_total_usdt_cents` содержит актуальную сумму. Согласуйте новую цену; для новой попытки нужен новый ключ |
| `shop.insufficient_balance`, `409` | Списания нет; доступны `balance_usdt_cents` и `required_usdt_cents`. После пополнения — осознанная новая попытка с новым ключом |
| `shop.out_of_stock`, `409` | Списания нет; `data.available_quantity` — остаток. После пополнения склада — новая попытка с новым ключом |
| `shop.fulfillment_unavailable`, `409` | Списание и выделение ключей отменены транзакцией. Перед новой попыткой проверьте товар с поддержкой |
| `shop.product_unavailable`, `404` | Товар скрыт, удалён, без цены или не относится к автоматической выдаче |
| `shop.terms_required`, `shop.email_required`, `shop.balance_disabled`, `403` | Покупка не совершена; отказ сохраняется за ключом. После устранения причины нужна осознанная новая попытка с новым `Idempotency-Key` |
| `401` / `403` | Проверьте срок/отзыв ключа, разрешения, исходящий IP, привязку аккаунта, блокировку и принятие правил |

Успешные покупки и определённые бизнес-ошибки покупки сохраняются за ключом без автоматического срока удаления. Повтор бизнес-ошибки вернёт тот же код с `data.replayed=true`, даже если баланс, цена, склад или принятие правил изменились. Ошибки формата до принятия покупки, ошибки `auth.*`, ограничения `rate.*`, отключённый/непривязанный аккаунт и временные `503` не фиксируют неуспешную покупку; после устранения причины допустим прежний ключ. Отказы `shop.terms_required`, `shop.email_required` и `shop.balance_disabled` сохраняются, как и ошибки цены, остатков и баланса. Даже при `503` ранее совершённая покупка могла сохраниться — ключ менять нельзя. Успех `success=true` не заменяет проверку статуса выдачи, особенно после действий поддержки.

`GET /orders/{order_id}` возвращает `data.order` той же формы. Чужой заказ, включая заказ другого API-клиента с тем же владельцем или покупку через сайт/бот, возвращает `404 shop.order_not_found`. Для отменённого администратором заказа статус `cancelled`, выдача скрыта; новых списаний при повторе не происходит.

`GET /orders?limit=100&after_id=0` возвращает `data.orders` и `data.next_after_id`. Порядок — возрастание ID заказа; передавайте `next_after_id` для следующей страницы до `null`. В списке только реально созданные заказы этого API-клиента, включая изменённые поддержкой. Отказанные попытки без заказа в список не входят. При утрате ID заказа повторите исходный POST с сохранённым ключом либо восстановите историю через список.

## Пример покупки на Python

В примере `requests` установлен у партнёра. Функция ограничивает число попыток; при исчерпании попыток нужно оставить задачу в очереди своего магазина и позднее вызвать её с тем же ключом и тем же телом. Не генерируйте новый ключ в обработчике повтора. До вызова проверьте подпись/статус оплаты в своей платёжной системе.

```python
import os
import time
import requests

def purchase_paid_order(saved_key, saved_body):
    headers = {"Authorization": "Bearer " + os.environ["SUPPLYME_API_KEY"],
               "Idempotency-Key": saved_key, "Accept-Language": "ru"}
    for attempt in range(4):
        try:
            response = requests.post("https://supply-me.ru/api/v1/orders",
                headers=headers, json=saved_body, timeout=(10, 30))
        except (requests.Timeout, requests.ConnectionError):
            time.sleep(min(2 ** attempt, 15))
            continue
        try:
            result = response.json()
        except ValueError:
            raise RuntimeError("Unknown result: queue the SAME key and body")
        if response.status_code == 429:
            # Queue this same operation for the indicated time; do not sleep a worker for a whole day.
            raise RuntimeError("Retry same key after " + response.headers.get("Retry-After", "60") + " seconds")
        if response.status_code >= 500:
            time.sleep(min(2 ** attempt, 15))
            continue
        if response.status_code not in (200, 201) or not result.get("success"):
            raise RuntimeError(f"{result.get('code')} request_id={result.get('request_id')}")
        order = result["data"]["order"]
        if order["status"] != "delivered" or order["delivery"] is None:
            raise RuntimeError("Order needs review; do not create another purchase")
        return order
    raise RuntimeError("Unknown result: queue the SAME key and body")

# These values must already be durably associated with your paid order:
# saved_key = "myshop-order-8457"
# saved_body = {"product_id": 42, "quantity": 1, "expected_total_usdt_cents": 1250}
# purchased = purchase_paid_order(saved_key, saved_body)
# Save purchased["id"] and delivery in your DB, then deliver to your customer exactly once.
```

В своём магазине также защитите повтор webhook и повтор выдачи: один оплаченный заказ → одна сохранённая операция закупки → одна выдача покупателю. Автоматическое списание у SupplyMe не заменяет эту защиту в сервисе партнёра. Проверяйте интеграцию на согласованном тестовом товаре: рабочий `POST /orders` действительно списывает баланс; sandbox/dry-run отсутствует.

## Помощь с интеграцией

API-ключ можно создать самостоятельно в «Профиль → API». Если нужна помощь с подключением или восстановлением результата покупки, обратитесь в поддержку SupplyMe и укажите request_id, метод, код ответа и время UTC. Не передавайте токены или содержимое выданных товаров в публичных сообщениях.
