Документация Reseller API

Полный справочник: каталоги и цены, двухфазные заказы, статусы доставки, подписанные вебхуки, пополнение депозита и коды ошибок.

Hand this API to an AI

Copy the prompt and paste it into ChatGPT, Claude or Cursor. It contains the whole spec: endpoints with response shapes, products, statuses, error codes explained, and the pitfalls people hit. That is enough to get working code instead of invented field names.

What you need

Client, order, polling, webhook

Language
An agent with internet access can just be given a link:https://hexpay.live/api/reseller/llms.txt

Basics

Base URL
https://hexpay.live
Authorization
Authorization: Bearer …

The path /api/reseller/v1/* equals /api/reseller/* — the version is pinned for new integrations, the unversioned path keeps working forever.

Публичный API для реселлеров: продавайте товары Hex (Telegram Stars, Telegram Premium) с автодоставкой. Оплата — с предоплаченного депозита по оптовой цене.

How it works

Модель заказа — двухфазная

POST /order/create фиксирует цену (депозит НЕ списывается) → POST /order/pay списывает депозит и ставит заказ в очередь доставки. Идемпотентность — по вашему custom_id (UUID): повтор create возвращает тот же заказ, повтор pay не списывает повторно. Повтор custom_id с ДРУГИМИ параметрами вернёт ПЕРВЫЙ заказ — для нового заказа используйте новый custom_id.

Статус доставки

Stars/Premium доставляются асинхронно (очередь). Опрашивайте GET /order/{custom_id} до статуса delivered/failed, либо укажите callback_url в create — придёт подписанный вебхук (см. ниже).

Где лежит выданный товар

В delivered_payload. У звёзд, Premium, подарков и Steam там техническое подтверждение доставки — товар ушёл получателю сам. А у нейросетей (key) и пакетов игровых кодов (codes) в этом поле лежит сам товар: ключ, который вы обязаны передать покупателю. У нейросетей рядом приходит activation_guide — инструкция по активации ключа (тот же текст есть в каталоге GET /neural, показывайте его ещё до покупки).

Вебхуки

Каналов два, и разница между ними только в адресе: тело и подпись одинаковые, поэтому обработчик у вас один.

Канал заказа. Передайте callback_url (только https) в order/create — на терминальном статусе этого заказа (delivered, failed, cancelled, а у Spotify ещё action_required) придёт POST с телом статуса.

Аккаунтный канал. Задайте один адрес в кабинете (раздел «Ключ и вебхуки») — и события будут приходить по всем заказам, без callback_url в каждом. Только здесь доступны события о НОВЫХ покупках: created (заказ создан, депозит ещё не списан) и paid (оплачен и встал в очередь), а также deposit — депозит пополнен. Список событий настраивается там же.

⚠️ created, paid и deposit на callback_url заказа не приходят никогда. Это сделано ради тех, кто уже подключён: их код писался под «пришёл вебхук — значит заказ закрыт», и начни мы слать туда «заказ создан», товар считался бы выданным до доставки. Аккаунтный адрес вы задаёте сами — само это действие и есть согласие получать больше.

Если один и тот же адрес указан и в кабинете, и в callback_url, событие придёт один раз, а не дважды.

Подпись. Заголовок X-Signature: sha256=<hmac_sha256(webhook_secret, raw_body)>, секрет — в кабинете. Считайте HMAC от СЫРОГО тела запроса, а не от разобранного JSON, и сравнивайте константным по времени сравнением.

Тело. У событий про заказ — ровно то же, что отдаёт GET /order/{custom_id}, плюс поле event. У deposit заказа нет, поэтому и полей заказа в нём нет: приходят custom_id пополнения (или null, если оно создано в кабинете), status, amount_nano, amount_rub, provider и новый balance_nano.

Poll остаётся источником истины — вебхук может не дойти.

Пополнение депозита

Депозит можно пополнить не выходя из кода. GET /deposit/providers вернёт доступные способы оплаты с комиссией, POST /deposit/create — ссылку на оплату, а GET /deposit/{custom_id} — что с пополнением стало.

Идемпотентность здесь такая же, как у заказов, по вашему custom_id, но с одним важным отличием: повтор с ДРУГИМИ параметрами вернёт 409, а не первый счёт. Для заказа тихо вернуть первый — правильно, а для денег вредно: вы получили бы старую ссылку на другую сумму и не поняли недостачу.

Статусы пополнения: pending — счёт выставлен, оплаты нет; crediting — оплачен, но зачисление ещё не закрепилось (повторится само); credited — деньги на депозите; failed / cancelled. Ориентируйтесь на credited: он означает запись в журнале, а журнал пишется одной транзакцией с балансом.

Снятые с продажи подарки

Telegram периодически убирает подарки из продажи, но отправлять их мы всё ещё умеем — и продаём вам. В каталоге они приходят тем же GET /gifts, с type: "deleted":

GET /api/reseller/gifts?type=deleted

Отличий от обычных подарков ровно два. Запас конечен и не пополняется, поэтому цена выше номинала звёзд — берите её из wholesale_price_nano позиции, а не считайте сами по звёздности. И remaining_count у них всегда null: сколько осталось, Telegram не сообщает.

Заказываются они как обычные — тем же order/create по gift_id, с теми же message и hide_name. Картинка для витрины: preview_url (статичная миниатюра) и animation_url (анимация .tgs); если has_preview: false, миниатюры у позиции нет вовсе — рисуйте заглушку по emoji, а не ходите за 404.

Что будет, если доставка сорвалась

Заказ переходит в failed, и депозит возвращается автоматически — тем же тиком, что и фиксация провала, без обращения в поддержку. В GET /order/{custom_id} это видно по полям refunded, refund_amount_nano, refunded_at.

Асинхронная доставка

У большинства товаров выдача занимает секунды. Spotify — исключение: заказ живёт в статусе processing минутами и часами, а в режиме spotify_pairing ещё и ждёт действия покупателя.

Нового статуса для этого мы НЕ вводили: status остаётся одним из семи известных значений, а подробности лежат в дополнительных полях, которые приходят только у таких товаров — delivery_state, action_required, eta_seconds. Для остальных товаров ответ не изменился ни на байт.

Сценарий spotify_pairing:

  1. order/paystatus: pending, затем processing.
  2. Когда поставщик выдаст ссылку — delivery_state: action_required и объект action_required со ссылкой и сроком. Если задан callback_url, придёт ещё и событие action_required (единственное новое событие вебхука).
  3. Покупатель открывает ссылку и подтверждает вход.
  4. status: delivered.

Ссылка живёт недолго. Протухла — мы выпускаем новую сами и присылаем action_required ещё раз, уже с новым url. Повторных событий на одну и ту же ссылку не будет.

Не дождались подтверждения до expires_at — заказ закрывается как failed с error_code: action_expired, депозит возвращается.

⚠️ Неизвестное событие вебхука игнорируйте, а не считайте ошибкой: список событий будет пополняться.

Частичная выдача

У товаров, где выдаются коды пачкой (apple), поставщик изредка отдаёт меньше карт, чем заказано. Такой заказ не считается провалом: статус delivered, в delivered_payload.codes — то, что реально пришло, а разница возвращается на депозит (refunded: true, сумма — в refund_amount_nano). Списанная сумма price_charged_nano при этом пересчитывается на выданное количество.

Ориентируйтесь на delivered_payload.delivered_quantity, а не на quantity: quantity — это то, что было заказано.

Реже заказ может быть отменён нами вручную (например, зависшая доставка, которую мы разбирали): тогда статус — cancelled с error_code: order_cancelled, а списанный депозит возвращается так же и виден в тех же полях. Обрабатывайте cancelled как терминальный статус наравне с failed.

Временные причины (кончился наш запас, сбой на нашей стороне) провалом НЕ считаются: заказ автоматически повторяется до 10 раз и деньги при этом не двигаются. Рефанд происходит только когда доставка окончательно невозможна.

Наличие товара

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

  • в каталогах /gifts и /neural есть поле `in_stock` — ориентируйтесь на него; в /games распроданные пакеты просто не приходят в списке;
  • наличие проверяется дважды — при order/create и ещё раз при order/pay, потому что между ними может пройти до получаса. Если позиция кончилась, вы получите 409 с error_code: out_of_stock и депозит не будет тронут;
  • если товар кончился уже после оплаты, заказ закрывается как failed с тем же кодом, и деньги возвращаются автоматически — мы не держим их в надежде, что товар вернётся.

Данные каталогов кэшируются на несколько минут, поэтому очень редкие позиции могут отдаваться как доступные ещё пару минут после того, как их разобрали.

Ошибки

У провалившегося заказа есть машиночитаемый error_code — ветвитесь по нему, а не по тексту: recipient_invalid, out_of_stock, product_unavailable, invalid_order_data, delivery_rejected, delivery_failed, delivery_in_review, temporary_failure, order_cancelled.

Версионирование

Рабочих префикса два и они равнозначны: /api/reseller/... и /api/reseller/v1/.... Для новых интеграций используйте v1 — при выходе v2 он продолжит вести себя по-старому. Путь без версии остаётся рабочим и всегда указывает на v1.

Rate limit

~60 запросов/мин на ключ. Превышение → 429 с заголовком Retry-After.

Products

The value of product when creating an order. Products with a subcatalog have no single price: the item comes from a separate catalog and is passed in the field shown.

starsTelegram Stars
quantity: количество звёзд (50…1 000 000)recipient: Telegram-юзернейм
premium_3mTelegram Premium 3 месяца
quantity: не передаётсяrecipient: Telegram-юзернейм
premium_6mTelegram Premium 6 месяцев
quantity: не передаётсяrecipient: Telegram-юзернейм
premium_12mTelegram Premium 12 месяцев
quantity: не передаётсяrecipient: Telegram-юзернейм
giftПодарки Telegram
quantity: не передаётся (цена = звёздность подарка)recipient: Telegram-юзернеймcatalog: /api/reseller/giftsgift_id
steamПополнение кошелька Steam
quantity: СУММА ПОПОЛНЕНИЯ В РУБЛЯХ (50…50 000)recipient: логин аккаунта Steam
neuralНейросети (ключи доступа)
quantity: не передаётсяrecipient: не нужен — ключ выдаётся вамcatalog: /api/reseller/neuralcode
gameИгры — коды и пополнение по логину
quantity: количество пакетов (1…10)recipient: не нужен — коды выдаются вамcatalog: /api/reseller/gamesproduct_ref
appleApple Gift Card
quantity: количество карт (1…10)recipient: не нужен — коды выдаются вамcatalog: /api/reseller/appleproduct_ref
spotify_pairingSpotify Premium — покупатель подтверждает вход сам
quantity: не передаётсяrecipient: не нужен — нужен email аккаунта в fieldscatalog: /api/reseller/spotifyproduct_ref
spotify_autoSpotify Premium — подключаем автоматически (нужен пароль)
quantity: не передаётсяrecipient: не нужен — нужны email и password в fieldscatalog: /api/reseller/spotifyproduct_ref

Endpoints 19

Order statuses

created

цена зафиксирована, депозит не списан

pending

оплачен, ждёт доставки

processing

доставляется

deliveredterminal

доставлен (терминальный)

failedterminal

доставка не удалась, депозит возвращён (терминальный)

cancelled

неоплаченный заказ истёк по TTL либо заказ отменён нами

rejectedterminal

на оплате не хватило депозита, списания не было (терминальный)

Poll the order until a terminal status. Do not treat an unknown status as an error — the list may grow.

Error codes

Branch on error_code, not on the text: we may reword the text, the code stays the same.

recipient_invaliddeposit refunded

Получателя не существует или он не может принять этот товар.

Проверьте юзернейм. До оплаты это ловит GET /check-recipient — он бесплатный.
Retry: Только после исправления получателя, с новым custom_id.
out_of_stockdeposit refunded

Позиция кончилась.

Обновите каталог и предложите покупателю другую позицию.
Retry: Когда товар вернётся в каталог с in_stock true.
product_unavailabledeposit refunded

Товар отключён — целиком или лично для вас.

Уберите его с витрины и напишите нам, если он нужен.
Retry: Нет, пока товар не включат обратно.
invalid_order_datadeposit refunded

Не хватает обязательного поля или оно не той формы.

Сверьтесь с required_field товара в /products и с формой product_ref в подкаталоге.
Retry: После исправления тела запроса, с новым custom_id.
delivery_rejecteddeposit refunded

Поставщик отказал по данным получателя.

Как правило, это неверный логин или регион. Проверьте данные с покупателем.
Retry: После исправления данных, с новым custom_id.
delivery_faileddeposit refunded

Доставка окончательно не удалась.

Деньги уже вернулись. Повтор имеет смысл — сбой мог быть разовым.
Retry: Да, с новым custom_id.
delivery_in_reviewmoney untouched

Доставка идёт дольше обычного, её смотрит человек. Заказ ещё НЕ провален.

Ничего не делайте и не возвращайте покупателю деньги — дождитесь терминального статуса.
Retry: Нет — заказ ещё живой.
temporary_failuredeposit refunded

Временный сбой у нас.

Депозит вернулся. Повторите позже.
Retry: Да, с новым custom_id.
order_cancelleddeposit refunded

Заказ отменён — истёк по TTL неоплаченным либо снят нами вручную.

Обрабатывайте как терминальный статус наравне с failed.
Retry: Да, с новым custom_id.
action_expireddeposit refunded

Покупатель не подтвердил действие в отведённый срок (асинхронные товары).

Депозит вернулся. Повторяйте, только когда покупатель готов подтвердить вход.
Retry: Да, когда покупатель на связи.

Activation guides

This text arrives in activation_guide in the order response — show it to the buyer right after delivery. You can also fetch it any time: GET /api/reseller/guide?product=…&lang=ru|en. For products with variants (Apple country, Spotify plan, a specific game) the text is refined by variant.

Pitfalls

Each item is about a real mistake we have already seen in live integrations.

custom_id — ваш ключ идемпотентности

Придумываете его вы (удобнее всего UUID). Повтор create с тем же custom_id вернёт ТОТ ЖЕ заказ, даже если параметры другие, а повтор pay не спишет депозит второй раз. Отсюда правило: новый заказ — новый custom_id, а ретрай упавшего запроса — со старым.

Опрос заказа — источник истины, вебхук — ускорение

Вебхук может не дойти: ваш сервер лежал, домен не резолвился, ответ пришёл не 2xx. Поэтому опрос GET /order/{custom_id} до терминального статуса нужен всегда, даже если callback_url задан.

refunded true бывает и у успешного заказа

При частичной выдаче (Apple отдал меньше карт, чем заказано) статус остаётся delivered, а разница за невыданное возвращается на депозит. Если ветвиться по одному refunded, такой заказ будет посчитан провальным, и покупателю вернут деньги за товар, который он уже получил. Смотрите на статус, сумму берите из refund_amount_nano.

delivery_in_review — заказ ещё живой

Этот error_code означает «доставка идёт дольше обычного, её смотрит человек», а не «провалилась». Деньги не возвращены, статус ещё не терминальный. Не возвращайте покупателю оплату по нему — дождитесь delivered или failed.

created и paid приходят только на аккаунтный вебхук

На callback_url конкретного заказа по-прежнему приходят только терминальные события. Если ждёте «заказ создан» или «заказ оплачен» — задайте адрес вебхука в кабинете, в разделе «Ключ и вебхуки». Тот же адрес в обоих местах не задваивает события.

Пополнение через API — не тот же custom_id, что у заказа

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

Незнакомые значения не ломают интеграцию

Список товаров, событий вебхука и полей пополняется. Незнакомое событие вебхука игнорируйте, незнакомый error_code трактуйте как обычный провал, новые поля в ответах не считайте ошибкой схемы.

Цены берите из API, а не из своей таблицы

Оптовая цена зависит от курса и ваших персональных условий и меняется. Витрину стройте по /products и подкаталогам, а точную сумму заказа — по ответу create, где она зафиксирована до expires_at.

Деньги приходят в двух видах — рубли и nano

Поля *_rub удобны для показа, *_nano — для арифметики: 1 ₽ = 1000 nano, целое число. Считайте в nano и округляйте один раз на выводе, иначе копейки разъедутся.

Асинхронные товары живут в processing долго

У Spotify доставка занимает минуты и часы, а в режиме spotify_pairing ещё и ждёт действия покупателя. Не считайте такой заказ зависшим по таймауту: ориентируйтесь на delivery_state, action_required и eta_seconds, которые приходят только у них.

Проверяйте получателя до оплаты

GET /check-recipient бесплатен и не трогает депозит. Это самый частый провал доставки (error_code recipient_invalid) и самый дешёвый в предотвращении.

Собираем анонимную аналитику посещений. Подробнее