Полный справочник: каталоги и цены, двухфазные заказы, статусы доставки, подписанные вебхуки, пополнение депозита и коды ошибок.
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.
Client, order, polling, webhook
https://hexpay.live/api/reseller/llms.txthttps://hexpay.liveAuthorization: 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) с автодоставкой. Оплата — с предоплаченного депозита по оптовой цене.
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:
order/pay → status: pending, затем processing.delivery_state: action_required и объект action_required со ссылкой и сроком. Если задан callback_url, придёт ещё и событие action_required (единственное новое событие вебхука).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.
~60 запросов/мин на ключ. Превышение → 429 с заголовком Retry-After.
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 Starspremium_3mTelegram Premium 3 месяцаpremium_6mTelegram Premium 6 месяцевpremium_12mTelegram Premium 12 месяцевgiftПодарки Telegram/api/reseller/giftsgift_idsteamПополнение кошелька SteamneuralНейросети (ключи доступа)/api/reseller/neuralcodegameИгры — коды и пополнение по логину/api/reseller/gamesproduct_refappleApple Gift Card/api/reseller/appleproduct_refspotify_pairingSpotify Premium — покупатель подтверждает вход сам/api/reseller/spotifyproduct_refspotify_autoSpotify Premium — подключаем автоматически (нужен пароль)/api/reseller/spotifyproduct_refcreatedцена зафиксирована, депозит не списан
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.
Branch on error_code, not on the text: we may reword the text, the code stays the same.
recipient_invaliddeposit refundedПолучателя не существует или он не может принять этот товар.
out_of_stockdeposit refundedПозиция кончилась.
product_unavailabledeposit refundedТовар отключён — целиком или лично для вас.
invalid_order_datadeposit refundedНе хватает обязательного поля или оно не той формы.
delivery_rejecteddeposit refundedПоставщик отказал по данным получателя.
delivery_faileddeposit refundedДоставка окончательно не удалась.
delivery_in_reviewmoney untouchedДоставка идёт дольше обычного, её смотрит человек. Заказ ещё НЕ провален.
temporary_failuredeposit refundedВременный сбой у нас.
order_cancelleddeposit refundedЗаказ отменён — истёк по TTL неоплаченным либо снят нами вручную.
action_expireddeposit refundedПокупатель не подтвердил действие в отведённый срок (асинхронные товары).
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.
Each item is about a real mistake we have already seen in live integrations.
Придумываете его вы (удобнее всего UUID). Повтор create с тем же custom_id вернёт ТОТ ЖЕ заказ, даже если параметры другие, а повтор pay не спишет депозит второй раз. Отсюда правило: новый заказ — новый custom_id, а ретрай упавшего запроса — со старым.
Вебхук может не дойти: ваш сервер лежал, домен не резолвился, ответ пришёл не 2xx. Поэтому опрос GET /order/{custom_id} до терминального статуса нужен всегда, даже если callback_url задан.
При частичной выдаче (Apple отдал меньше карт, чем заказано) статус остаётся delivered, а разница за невыданное возвращается на депозит. Если ветвиться по одному refunded, такой заказ будет посчитан провальным, и покупателю вернут деньги за товар, который он уже получил. Смотрите на статус, сумму берите из refund_amount_nano.
Этот error_code означает «доставка идёт дольше обычного, её смотрит человек», а не «провалилась». Деньги не возвращены, статус ещё не терминальный. Не возвращайте покупателю оплату по нему — дождитесь delivered или failed.
На callback_url конкретного заказа по-прежнему приходят только терминальные события. Если ждёте «заказ создан» или «заказ оплачен» — задайте адрес вебхука в кабинете, в разделе «Ключ и вебхуки». Тот же адрес в обоих местах не задваивает события.
У пополнений своё пространство ключей. Повтор custom_id пополнения с другой суммой или другим провайдером вернёт 409, а не старый счёт: тихо отдать прежнюю ссылку на другую сумму значило бы оставить вас с недостачей без объяснимой причины.
Список товаров, событий вебхука и полей пополняется. Незнакомое событие вебхука игнорируйте, незнакомый error_code трактуйте как обычный провал, новые поля в ответах не считайте ошибкой схемы.
Оптовая цена зависит от курса и ваших персональных условий и меняется. Витрину стройте по /products и подкаталогам, а точную сумму заказа — по ответу create, где она зафиксирована до expires_at.
Поля *_rub удобны для показа, *_nano — для арифметики: 1 ₽ = 1000 nano, целое число. Считайте в nano и округляйте один раз на выводе, иначе копейки разъедутся.
У Spotify доставка занимает минуты и часы, а в режиме spotify_pairing ещё и ждёт действия покупателя. Не считайте такой заказ зависшим по таймауту: ориентируйтесь на delivery_state, action_required и eta_seconds, которые приходят только у них.
GET /check-recipient бесплатен и не трогает депозит. Это самый частый провал доставки (error_code recipient_invalid) и самый дешёвый в предотвращении.