Autobuyer 4.4 Cart Modifier 4.0 Документация API
// API v2.0

Powpowders Autobuyer
API Documentation v2.0

Встроенный асинхронный HTTP-сервер с очередью задач, неблокирующей моделью ввода и webhook-нотификациями. Никаких внешних процессов — поднимается прямо внутри .exe.

Общая схема
┌─────────────────────────────────────────────────┐
│              Powpowders Autobuyer.exe            │
│                                                  │
│  ┌──────────────┐     ┌────────────────────┐    │
│  │  HTTP Server │────▶│    TaskStore       │    │
│  │  :7788       │     │  (thread-safe TTL) │    │
│  └──────┬───────┘     └────────┬───────────┘    │
│         │                      │                 │
│         │             ┌────────▼───────────┐    │
│         │             │   TaskExecutor     │    │
│         │             │  worker-thread     │    │
│         │             │  asyncio event loop│    │
│         │             └────────┬───────────┘    │
│         │                      │                 │
│         │             ┌────────▼───────────┐    │
│         │             │  InputManager      │    │
│         │             │  (blocking Queue)  │    │
│         │             └────────────────────┘    │
└─────────────────────────────────────────────────┘
         │ webhooks (non-blocking, отдельный поток)
         ▼
   внешний сервер клиента
Модель выполнения задач

Сервер реализует асинхронную очередь задач с изолированным event loop на каждую задачу:

• Каждый POST /tasks/sale помещает задачу в queue.Queue
• Воркер-тред извлекает задачу и запускает её в asyncio.new_event_loop()
  — полностью изолированно от других задач
• Параллельные заказы исполняются в параллельных тредах,
  каждый со своим event loop
• Задачи в статусе waiting_input не блокируют воркер —
  они ждут в queue.Queue.get(timeout=...) внутри своего треда,
  пока клиент не пришлёт ответ через POST /tasks/{id}/input
Неблокирующая модель ввода (InputManager)
OrderProcessor               InputManager              API Client
      │                           │                        │
      │── register(task_id) ─────▶│                        │
      │── set_waiting_input() ────▶│ (status → waiting_input)
      │                           │                        │
      │   [заблокирован           │◀── provide_input() ────│
      │    Queue.get(timeout)]    │     (POST /input)      │
      │                           │                        │
      │◀── value ─────────────────│                        │
      │── set_running() ──────────▶│                        │
      │   [продолжает работу]     │                        │
Webhook-диспетчер

Все исходящие вебхуки отправляются неблокирующе — каждый вызов _fire_webhook() запускает отдельный daemon-тред с urllib.request, не задерживая выполнение задачи.

// НАСТРОЙКА

Настройка и аутентификация

Параметры сервера
ПараметрПо умолчаниюОписание
Bind address127.0.0.1Только loopback — не экспонируется наружу без reverse-proxy
Порт7788Настраивается в GUI вкладки API
TTL задач3600 секЗавершённые задачи очищаются через 1 час
Таймаут ввода300 секМаксимальное время ожидания waiting_input
Аутентификация

Все запросы требуют заголовок:

X-API-Key: YOUR_SECRET_KEY
Отсутствие или неверный ключ → 401 Unauthorized. Если ключ не задан — аутентификация отключена (не рекомендуется).
Проверка связи
curl
curl -H "X-API-Key: YOUR_KEY" http://127.0.0.1:7788/api/v1/ping
# → {"pong": true, "version": "2.0"}
// ЭНДПОИНТЫ

Справочник эндпоинтов

GET /api/v1/ping Health-check
{
  "pong": true,
  "version": "2.0"
}
GET /api/v1/status Состояние системы

Состояние системы: мониторинг FunPay, пул Razer-аккаунтов, слоты задач.

response
{
  "api_version": "2.0",
  "monitoring": true,
  "razer_accounts": 3,
  "cards": 2,
  "tasks": {
    "pending": 0,
    "running": 2,
    "waiting": 1
  }
}
GET /api/v1/products Список товаров

Список товаров, сконфигурированных в приложении.

response
{
  "razer": [
    {"name": "Fortnite Crew", "link": "https://store.epicgames.com/..."},
    {"name": "1000 V-Bucks",  "link": "https://store.epicgames.com/..."}
  ],
  "card": [
    {"name": "Fortnite Crew Card", "link": "https://store.epicgames.com/..."}
  ]
}
GET /api/v1/stats?period=... Статистика продаж

Агрегированная статистика продаж.

ПараметрДопустимые значения
periodtoday, 7d, 30d, all
response
{
  "period": "7d",
  "total": 42,
  "success": 40,
  "fail": 2,
  "gross": 15000.0,
  "expenses": 12000.0,
  "commission_3pct": 450.0,
  "net": 2550.0,
  "rate_pct": 95.2
}
GET /api/v1/tasks Список задач

Список задач с опциональной фильтрацией по статусу, отсортированный по created_at desc.

ПараметрПо умолчаниюОписание
limit50Максимум записей в ответе
status(все)Фильтр по статусу конечного автомата
GET /api/v1/tasks?status=waiting_input&limit=10
GET /api/v1/tasks/{task_id} Состояние задачи

Полное состояние задачи включая накопленный лог сообщений.

response
{
  "id":           "550e8400-e29b-41d4-a716-446655440000",
  "type":         "sale",
  "status":       "waiting_auth",
  "auth_url":     "https://www.epicgames.com/activate?userCode=ABCDE",
  "input_needed": null,
  "messages": [
    {"text": "🔄 Начинаем обработку заказа...", "ts": 1710000001.0},
    {"text": "🔐 Ссылка для входа отправлена",  "ts": 1710000005.0}
  ],
  "result":     null,
  "error":      null,
  "created_at": 1710000000.0,
  "updated_at": 1710000010.0,
  "payload": {
    "product_name": "Fortnite Crew",
    "payment_type": "razer",
    "buyer":        "username"
  }
}
Статусы конечного автомата
СтатусОписание
pendingЗадача в очереди, воркер ещё не взял
runningАктивно выполняется в event loop
waiting_authЗаблокирована: ждёт авторизации покупателя в Epic (device code flow)
waiting_inputЗаблокирована: ждёт внешнего ввода через POST /input (email-код, PIN)
doneТерминальное состояние — успех
errorТерминальное состояние — ошибка
cancelledТерминальное состояние — отменена клиентом
POST /api/v1/tasks/sale Создать задачу продажи

Создать задачу продажи. Возвращает 202 Accepted немедленно — задача ставится в очередь и выполняется асинхронно.

ПолеТипОбяз.Описание
product_namestringТочное имя товара из /api/v1/products
payment_typestring"razer" или "card"
product_typestring"one_time" (по умолчанию) или "subscription"
chat_idintID чата для fallback-сообщений через FunPay
buyerstringИдентификатор покупателя (для логов)
order_idstringИдемпотентный внешний ID заказа
exchange_codestringEpic OAuth exchange code — позволяет пропустить device code flow
proxystringПрокси для этого заказа: http://user:pass@host:port
message_webhook_urlstringEndpoint для получения сообщений покупателю (event: message)
status_webhook_urlstringEndpoint для статусных событий (waiting_auth, waiting_input, status_change)
response 202
{
  "task_id": "550e8400-e29b-41d4-a716-446655440000",
  "status":  "pending"
}
POST /api/v1/tasks/region Смена региона без покупки

Создать задачу смены региона Epic без выполнения покупки. Проводит полный device code flow + смена региона на TR.

Request body: те же поля что у /tasks/sale, кроме product_name, payment_type, product_type, exchange_code.

POST /api/v1/tasks/{task_id}/input Передать ввод

Разблокировать задачу в состоянии waiting_input — передать введённый пользователем код или PIN. Принимает поле value, input или code (любое из трёх).

request
{"value": "847291"}
response 200
{"accepted": true, "task_id": "550e8400-..."}
Response 409 — задача не ждёт ввода:
{"error": "Task not waiting for input (status: running)", "status": "running"}
DELETE /api/v1/tasks/{task_id} Отменить задачу

Отменить задачу. Допустимо только для pending, waiting_auth, waiting_input — задачу в состоянии running прервать нельзя.

{"cancelled": true}
GET /api/v1/pins Список Razer PIN-кодов

Получить список всех доступных Razer PIN-кодов с их статусами.

response
{
  "pins": [
    {"code": "RAZER-XXXX-XXXX", "balance": 500, "used": false, "account": "razer_acc_1"},
    {"code": "RAZER-YYYY-YYYY", "balance": 250, "used": true, "account": "razer_acc_2"}
  ]
}
POST /api/v1/pins Добавить Razer PIN

Добавить новый Razer PIN-код в систему.

ПолеТипОбяз.Описание
codestringPIN-код (например RAZER-XXXX-XXXX-XXXX)
accountstringНазвание аккаунта Razer для привязки
request
{"code": "RAZER-XXXX-XXXX-XXXX", "account": "razer_acc_1"}
response 201
{"success": true, "message": "PIN добавлен"}
DELETE /api/v1/pins/{code} Удалить Razer PIN

Удалить Razer PIN-код из системы.

response 200
{"success": true, "message": "PIN удалён"}
// КОНЦЕПЦИИ

Конечный автомат задачи

                POST /tasks/sale
                      │
                      ▼
                   pending  ◀──── задача в очереди TaskExecutor
                      │
              воркер-тред взял
                      │
                      ▼
                   running
                      │
          ┌───────────┴────────────┐
          │                        │
          ▼                        ▼
    waiting_auth            waiting_input
  [device code flow]     [email-код / PIN]
  блокирует тред,        блокирует тред,
  ждёт Epic API          ждёт POST /input
          │                        │
   Epic авторизован        input получен
          │                        │
          └───────────┬────────────┘
                      ▼
                   running
                      │
             ┌────────┴────────┐
             ▼                 ▼
           done              error
      [терминальный]    [терминальный]

  + cancelled — из pending/waiting_* через DELETE
Polling vs Webhooks

Polling — простое опрашивание каждые N секунд:

python — polling
import time, requests

def poll_task(task_id: str, interval: float = 2.0):
    HEADERS  = {"X-API-Key": "YOUR_KEY"}
    BASE_URL = "http://127.0.0.1:7788/api/v1"
    seen_msgs = 0

    while True:
        r    = requests.get(f"{BASE_URL}/tasks/{task_id}", headers=HEADERS, timeout=5)
        task = r.json()

        # Дренируем накопленные сообщения для покупателя
        for msg in task["messages"][seen_msgs:]:
            deliver_to_user(msg["text"])
        seen_msgs = len(task["messages"])

        match task["status"]:
            case "waiting_auth":
                notify_user_auth_url(task["auth_url"])

            case "waiting_input":
                code = ask_user_for_input(task["input_needed"])
                requests.post(
                    f"{BASE_URL}/tasks/{task_id}/input",
                    headers=HEADERS, json={"value": code}
                )

            case "done":
                return task["result"]

            case "error" | "cancelled":
                raise RuntimeError(task.get("error") or task["status"])

        time.sleep(interval)
Webhooks — push-модель, рекомендуется для продакшна (см. раздел Webhook-нотификации).
// WEBHOOK

Webhook-нотификации

Все события доставляются неблокирующим POST в отдельном daemon-треде. Гарантия доставки — best-effort (таймаут 5 секунд, без retry).

waiting_auth — требуется авторизация Epic

Событие эмитируется сразу после генерации device code. Покупатель должен перейти по auth_url и подтвердить вход.

{
  "event":    "waiting_auth",
  "task_id":  "550e8400-...",
  "auth_url": "https://www.epicgames.com/activate?userCode=ABCDE",
  "ts":       1710000005.123
}
waiting_input — требуется ввод от покупателя

Эмитируется когда OrderProcessor запрашивает внешний ввод (6-значный email-код Epic, PIN родительского контроля и т.д.). Клиент обязан вызвать POST /tasks/{id}/input до истечения таймаута (300 сек).

{
  "event":        "waiting_input",
  "task_id":      "550e8400-...",
  "input_needed": "Введите 6-значный код подтверждения из письма Epic Games",
  "ts":           1710000060.0
}
message — сообщение для покупателя

Эмитируется каждый раз, когда OrderProcessor хочет что-то сообщить покупателю. Если message_webhook_url не указан — сообщение пишется в FunPay чат через chat_id (fallback).

{
  "event":   "message",
  "task_id": "550e8400-...",
  "text":    "✅ Вход подтверждён! Начали обработку заказа.",
  "ts":      1710000047.0
}
status_change — изменение терминального статуса

Эмитируется при переходе в done, error или cancelled.

{
  "event":   "status_change",
  "task_id": "550e8400-...",
  "status":  "done",
  "result": {
    "success":           true,
    "epic_display_name": "CoolGamer2024",
    "epic_account_id":   "abc123def456",
    "country":           "TR",
    "order_id":          "ORDER-9876"
  },
  "error": null,
  "ts":    1710000180.0
}
Ответ вебхук-сервера не имеет значения — API не ждёт и не анализирует его.
// ИНТЕГРАЦИИ

Примеры интеграций

Telegram-бот (Python / aiogram 3)

Полная реализация: приём заказа → device code flow → интерактивный ввод кода → финальный статус.

Зависимости
pip install aiogram aiohttp
Архитектура
Telegram ──▶ aiogram polling ──▶ /buy → POST /tasks/sale
                                              │
Autobuyer ──▶ webhook :8080   ◀──────── status events
     │
     └── message event ──▶ bot.send_message(chat_id, text)
     └── waiting_auth  ──▶ bot.send_message(chat_id, auth_url)
     └── waiting_input ──▶ bot.send_message(chat_id, prompt)
                               покупатель пишет код
                           ──▶ POST /tasks/{id}/input
Код
tg_bot.py
import asyncio
import aiohttp
from aiohttp import web
from aiogram import Bot, Dispatcher, Router, F
from aiogram.types import Message
from aiogram.filters import Command
from aiogram.fsm.storage.memory import MemoryStorage

API_BASE     = "http://127.0.0.1:7788/api/v1"
API_KEY      = "YOUR_KEY"
BOT_TOKEN    = "YOUR_TELEGRAM_BOT_TOKEN"
WEBHOOK_HOST = "https://your-server.example.com"
WEBHOOK_PORT = 8080

bot    = Bot(token=BOT_TOKEN)
dp     = Dispatcher(storage=MemoryStorage())
router = Router()
dp.include_router(router)

HEADERS = {"X-API-Key": API_KEY, "Content-Type": "application/json"}

# task_id → chat_id  (для доставки сообщений)
# "input:{chat}" → task_id  (chat ожидает ввода)
sessions: dict[str, int | str] = {}

async def api(method: str, path: str, **kwargs) -> dict:
    async with aiohttp.ClientSession() as s:
        async with getattr(s, method)(
            f"{API_BASE}{path}", headers=HEADERS, **kwargs
        ) as r:
            return await r.json()

@router.message(Command("buy"))
async def cmd_buy(msg: Message):
    resp = await api("post", "/tasks/sale", json={
        "product_name":        "Fortnite Crew",
        "payment_type":        "razer",
        "chat_id":             msg.chat.id,
        "buyer":               msg.from_user.username or str(msg.from_user.id),
        "order_id":            f"TG-{msg.chat.id}-{msg.message_id}",
        "message_webhook_url": f"{WEBHOOK_HOST}/wh/message",
        "status_webhook_url":  f"{WEBHOOK_HOST}/wh/status",
    })
    task_id = resp.get("task_id")
    if not task_id:
        await msg.answer("❌ Не удалось создать задачу. Попробуйте позже.")
        return
    sessions[task_id] = msg.chat.id
    await msg.answer("✅ Заказ принят в обработку.\nЧерез несколько секунд придёт ссылка для входа в Epic Games.")

@router.message(F.text)
async def handle_text(msg: Message):
    chat_id = msg.chat.id
    task_id = sessions.get(f"input:{chat_id}")
    if not task_id: return
    resp = await api("post", f"/tasks/{task_id}/input", json={"value": msg.text.strip()})
    if resp.get("accepted"):
        sessions.pop(f"input:{chat_id}", None)
        await msg.answer("✅ Принято, продолжаем обработку...")

async def wh_message(request: web.Request) -> web.Response:
    data    = await request.json()
    chat_id = sessions.get(data.get("task_id"))
    if chat_id: await bot.send_message(chat_id, data.get("text", ""))
    return web.Response(status=200)

async def wh_status(request: web.Request) -> web.Response:
    data    = await request.json()
    task_id = data.get("task_id")
    event   = data.get("event")
    chat_id = sessions.get(task_id)
    if not chat_id: return web.Response(status=200)

    if event == "waiting_auth":
        await bot.send_message(chat_id,
            f"🔐 *Необходима авторизация Epic Games*\n\nПерейдите по ссылке:\n{data['auth_url']}",
            parse_mode="Markdown")
    elif event == "waiting_input":
        sessions[f"input:{chat_id}"] = task_id
        await bot.send_message(chat_id, f"✏️ *Требуется ввод*\n{data.get('input_needed','Введите код:')}", parse_mode="Markdown")
    elif event == "status_change":
        status = data.get("status")
        if status == "done":
            r = data.get("result", {})
            await bot.send_message(chat_id, f"✅ *Заказ выполнен!*\nАккаунт: `{r.get('epic_display_name')}`\nРегион: `{r.get('country')}`", parse_mode="Markdown")
        elif status == "error":
            await bot.send_message(chat_id, f"❌ *Ошибка*\n`{data.get('error')}`", parse_mode="Markdown")
        sessions.pop(task_id, None)
    return web.Response(status=200)

async def main():
    app = web.Application()
    app.router.add_post("/wh/message", wh_message)
    app.router.add_post("/wh/status",  wh_status)
    runner = web.AppRunner(app)
    await runner.setup()
    await web.TCPSite(runner, "0.0.0.0", WEBHOOK_PORT).start()
    await dp.start_polling(bot, skip_updates=True)

if __name__ == "__main__":
    asyncio.run(main())
Discord-бот (discord.py)
discord_bot.py
import asyncio
import discord, aiohttp
from discord.ext import commands
from aiohttp import web

API_BASE     = "http://127.0.0.1:7788/api/v1"
API_KEY      = "YOUR_KEY"
BOT_TOKEN    = "YOUR_DISCORD_BOT_TOKEN"
WEBHOOK_HOST = "https://your-server.example.com"
WEBHOOK_PORT = 8080

intents = discord.Intents.default()
intents.message_content = True
bot = commands.Bot(command_prefix="!", intents=intents)
HEADERS = {"X-API-Key": API_KEY, "Content-Type": "application/json"}

channel_tasks: dict[int | str, int | str] = {}
awaiting_input: dict[int, str] = {}

@bot.command(name="buy")
async def buy(ctx: commands.Context, *, product: str = "Fortnite Crew"):
    async with aiohttp.ClientSession() as s:
        async with s.post(f"{API_BASE}/tasks/sale", json={
            "product_name":        product,
            "payment_type":        "razer",
            "buyer":               ctx.author.name,
            "order_id":            f"DC-{ctx.message.id}",
            "message_webhook_url": f"{WEBHOOK_HOST}/wh/message",
            "status_webhook_url":  f"{WEBHOOK_HOST}/wh/status",
        }, headers=HEADERS) as r:
            resp = await r.json()
    task_id = resp.get("task_id")
    if task_id:
        channel_tasks[task_id]        = ctx.channel.id
        channel_tasks[ctx.channel.id] = task_id
        await ctx.send(f"✅ Задача `{task_id[:8]}...` создана.")

@bot.listen("on_message")
async def intercept_input(message: discord.Message):
    if message.author.bot or message.content.startswith("!"): return
    task_id = awaiting_input.pop(message.channel.id, None)
    if not task_id: return
    async with aiohttp.ClientSession() as s:
        async with s.post(f"{API_BASE}/tasks/{task_id}/input",
                         json={"value": message.content.strip()}, headers=HEADERS) as r:
            resp = await r.json()
    if resp.get("accepted"): await message.add_reaction("✅")
Digiseller

Digiseller нотифицирует продавца о продаже через HTTP-уведомление на notify URL.

Настройка: Товары → Настройки товара → Уведомление продавцу → URL: https://your-server/ds/notify

Покупатель оплатил → Digiseller POST /ds/notify
                                      │
                             POST /tasks/sale
                                      │
                          Autobuyer выполняет заказ
                                      │
                  event=message → Digiseller Debates API
                  event=done   → логируем / закрываем тикет
digiseller_integration.py (ключевые части)
PRODUCT_MAP = {
    "1234567": "Fortnite Crew",
    "1234568": "1000 V-Bucks",
}

async def ds_notify(request: web.Request):
    # GET или POST в зависимости от настройки Digiseller
    unique_code = params.get("uniquecode", "")
    product_id  = params.get("id_goods", "")
    # → POST /api/v1/tasks/sale с order_id=f"DS-{unique_code}"

async def ds_message(request: web.Request):
    # Digiseller Debates API v2 — покупатель видит в личном кабинете
    await s.post("https://api.digiseller.ru/api/debates/v2",
        json={"id_goods": unique_code, "message": text, "secret_key": DIGISELLER_KEY})
Playerok

Playerok нотифицирует о подтверждённых заказах через webhook при статусе CONFIRMED.

Ограничение платформы: Playerok не предоставляет API для отправки сообщений покупателю. auth_url и коды необходимо доставлять через сторонний канал (Telegram, email). Рекомендуется связывать заказ с tg_chat_id покупателя.
playerok_integration.py (ключевые части)
PRODUCT_MAP = {
    "Fortnite Crew подписка": "Fortnite Crew",
    "1000 В-баксов":          "1000 V-Bucks",
}

async def playerok_notify(request: web.Request):
    if data.get("status") != "CONFIRMED": return
    # tg_chat_id передаётся в description лота
    # → POST /api/v1/tasks/sale с order_id=f"PO-{order_id}"
ЮKassa

ЮKassa нотифицирует через IPN о событиях платёжного жизненного цикла.

Создание платежа с метаданными
python
payment = yookassa.Payment.create({
    "amount":       {"value": "299.00", "currency": "RUB"},
    "confirmation": {"type": "redirect", "return_url": "https://your-site.ru/thanks"},
    "capture":      True,
    "description":  "Fortnite Crew",
    "metadata": {
        "product_name": "Fortnite Crew",
        "payment_type": "razer",
        "buyer":        "username",
        "tg_chat_id":   "123456789",
    }
})
# → редиректим на payment.confirmation.confirmation_url
IPN-обработчик: при событии payment.succeeded — POST /api/v1/tasks/sale с данными из metadata.
Верификация IP: ЮKassa не использует HMAC — нотификации приходят только с IP-адресов ЮKassa. Проверяй IP в продакшне (whitelist: 185.71.76.0/27, 185.71.77.0/27, 77.75.153.0/25 и др.).
Lava.ru
Создание инвойса
POST https://api.lava.ru/business/invoice/create
{
  "sum":       299,
  "orderId":   "unique-order-id",
  "hookUrl":   "https://your-server.example.com/lava/notify",
  "successUrl": "https://your-site.ru/thanks",
  "comment":  "Fortnite Crew",
  "customFields": {
    "product_name": "Fortnite Crew",
    "tg_chat_id":   "123456789",
    "buyer":        "username"
  }
}

HMAC-SHA256 верификация подписи через заголовок Signature. При статусе "success" — POST /api/v1/tasks/sale с данными из custom_fields.

n8n / Make
n8n — workflow для приёма заказа
[Webhook trigger]
      │  {product, buyer, tg_chat_id}
      ▼
[HTTP Request]
  POST http://127.0.0.1:7788/api/v1/tasks/sale
  Headers: X-API-Key: ...
  Body: {product_name, payment_type, buyer, status_webhook_url, message_webhook_url}
      │
      │  {task_id, status: "pending"}
      ▼
[Set variable: task_id]
      │
      ▼
[Respond to Webhook: 200 OK]
n8n — workflow для обработки событий
[Webhook trigger: /wh/autobuyer]
      │  {event, task_id, ...}
      ▼
[Switch по event]
  ├── "waiting_auth"  → [Telegram: send auth_url покупателю]
  ├── "waiting_input" → [Telegram: запросить код]
  │                     [Webhook: /wh/input/{task_id}]
  │                          → [HTTP: POST /tasks/{id}/input]
  └── "status_change" → [Switch по status]
                          ├── "done"  → [Telegram: ✅]
                          └── "error" → [Telegram: ❌]
Make (Integromat) — модули
МодульНастройка
1Webhooks → Custom webhookПринимает заказ от магазина
2HTTP → Make a requestPOST /tasks/sale, передаём webhook URL
3Tools → Set variableСохраняем task_id из ответа
4Webhooks → Custom webhookПринимает события от Autobuyer
5RouterВетвление по полю event
6Telegram → Send a messageДоставка auth_url / сообщений покупателю
7HTTP → Make a requestPOST /tasks/{id}/input после получения кода
// ИНФРАСТРУКТУРА

Reverse-proxy (nginx + TLS)

API слушает только на 127.0.0.1 — для внешней доступности вебхук-сервера нужен TLS-терминирующий reverse-proxy.

Установка
sudo apt install nginx certbot python3-certbot-nginx -y
sudo certbot --nginx -d your-domain.ru
nginx конфиг
/etc/nginx/sites-available/autobuyer
server {
    listen 443 ssl http2;
    server_name your-domain.ru;

    ssl_certificate     /etc/letsencrypt/live/your-domain.ru/fullchain.pem;
    ssl_certificate_key /etc/letsencrypt/live/your-domain.ru/privkey.pem;
    ssl_protocols       TLSv1.2 TLSv1.3;

    # Публичные вебхуки от платёжных систем
    location /wh/ {
        proxy_pass         http://127.0.0.1:8080;
        proxy_read_timeout 30s;
        proxy_send_timeout 10s;
    }

    # API Autobuyer — только с IP-whitelist
    location /api/ {
        allow  1.2.3.4;    # твой офис / VPS
        deny   all;
        proxy_pass         http://127.0.0.1:7788;
        proxy_read_timeout 120s;
    }
}

server {
    listen 80;
    server_name your-domain.ru;
    return 301 https://$host$request_uri;
}
sudo nginx -t && sudo systemctl reload nginx
// ДИАГНОСТИКА

Коды ошибок и диагностика

HTTP статусы
КодОписание
200Успех
202Задача принята в очередь (асинхронная обработка)
400Невалидный запрос — отсутствуют обязательные поля
401Неверный или отсутствующий X-API-Key
404Эндпоинт или задача не найдены
409Конфликт состояния — нельзя отменить running-задачу или подать ввод в не-waiting_input задачу
Поле error в терминальной задаче
ЗначениеПричина
auth_timeoutПокупатель не авторизовался в Epic за auth_timeout_minutes минут
no_razer_accountПул Razer-аккаунтов пуст — все слоты заняты или не настроены
no_card_availableНет активных банковских карт в конфиге
invalid_exchange_codeПереданный exchange_code истёк или невалиден
unknown_task_typeНеизвестный type задачи (внутренняя ошибка)
Диагностика
curl
# Все активные задачи
curl -H "X-API-Key: KEY" "http://127.0.0.1:7788/api/v1/tasks?status=running"

# Задачи ожидающие ввода
curl -H "X-API-Key: KEY" "http://127.0.0.1:7788/api/v1/tasks?status=waiting_input"

# Статистика за сегодня
curl -H "X-API-Key: KEY" "http://127.0.0.1:7788/api/v1/stats?period=today"

# Состояние пула аккаунтов
curl -H "X-API-Key: KEY" "http://127.0.0.1:7788/api/v1/status"
// ПРИМЕРЫ

Полные примеры payload

Заказ с предавторизованным пользователем (exchange_code flow)

Если клиент уже имеет Epic exchange_code — device code flow и waiting_auth пропускаются, задача сразу переходит в running:

POST /api/v1/tasks/sale
{
  "product_name":        "1000 V-Bucks",
  "payment_type":        "card",
  "product_type":        "one_time",
  "buyer":               "username123",
  "order_id":            "MYSHOP-9876",
  "exchange_code":       "abc123def456ghi789...",
  "message_webhook_url": "https://your-server.ru/wh/message",
  "status_webhook_url":  "https://your-server.ru/wh/status"
}
Заказ через прокси (изоляция по IP)
POST /api/v1/tasks/sale
{
  "product_name": "Fortnite Crew",
  "payment_type": "razer",
  "buyer":        "username",
  "order_id":     "MYSHOP-1234",
  "proxy":        "http://proxyuser:proxypass@gate.proxy.ru:8080"
}
Смена региона без покупки
POST /api/v1/tasks/region
{
  "buyer":               "username",
  "order_id":            "REGION-001",
  "message_webhook_url": "https://your-server.ru/wh/message",
  "status_webhook_url":  "https://your-server.ru/wh/status"
}
Полный объект завершённой задачи
GET /api/v1/tasks/{id} — response
{
  "id":     "550e8400-e29b-41d4-a716-446655440000",
  "type":   "sale",
  "status": "done",
  "messages": [
    {"text": "🔄 Начинаем обработку заказа. Ожидайте ссылку для входа...", "ts": 1710000001.0},
    {"text": "✅ Вход в аккаунт подтверждён! Начали делать ваш заказ!",   "ts": 1710000045.0},
    {"text": "✅ Заказ выполнен!",                                           "ts": 1710000180.0}
  ],
  "result": {
    "success":           true,
    "epic_display_name": "CoolGamer2024",
    "epic_account_id":   "abc123def456",
    "country":           "TR",
    "order_id":          "MYSHOP-9876"
  },
  "error":      null,
  "created_at": 1710000000.0,
  "updated_at": 1710000180.0
}