📦 API Доставка товаров

← Назад к документации

🎯 Обзор системы API доставки

API доставка товаров позволяет выдавать купленные товары не через RCON команды Minecraft, а через HTTP запросы к вашему собственному API серверу. Это дает вам полную гибкость в обработке доставки товаров.

💡 Для чего это нужно?

  • Интеграция с собственной системой управления игровыми товарами
  • Выдача товаров в играх, отличных от Minecraft
  • Сложная бизнес-логика обработки покупок
  • Централизованное управление доставкой через единый API
  • Интеграция с существующим backend вашего проекта

✨ Возможности

  • ✅ REST JSON API с Bearer token аутентификацией
  • ✅ Глобальная конфигурация для всех товаров
  • ✅ Переопределение настроек для отдельных товаров
  • ✅ Автоматическая доставка через POST запросы
  • ✅ Rollback (отмена) при возврате средств
  • ✅ Автоматические повторные попытки при ошибках
  • ✅ Полное логирование всех запросов
  • ✅ Совместимость с RCON (можно использовать оба метода)

🔄 Как работает система доставки

Когда пользователь покупает товар с включенной API доставкой, бот выполняет следующие действия:

1. Пользователь оплачивает товар через бота
2. Бот получает подтверждение оплаты от платежной системы
3. Бот формирует JSON запрос с данными о покупке
4. Отправляет POST запрос на ваш delivery_endpoint
5. Ваш сервер обрабатывает запрос и выдает товар игроку
6. Ваш сервер возвращает успешный ответ (HTTP 200-299)
7. Бот уведомляет пользователя об успешной доставке

⚠️ Важно: При ошибке

Если ваш сервер возвращает ошибку (не 2xx код) или не отвечает, бот автоматически повторяет запрос несколько раз с увеличивающимися задержками (см. раздел Повторные запросы).

Отмена доставки (Rollback)

При возврате средств (refund) или отмене заказа, бот отправляет POST запрос на ваш rollback_endpoint с теми же данными. Ваш сервер должен отменить выдачу товара.

⚙️ Настройка конфигурации

Глобальная конфигурация (config.yml)

Добавьте секцию api_delivery на верхнем уровне config.yml:

# Глобальная конфигурация API доставки
api_delivery:
  enabled: true                          # Включить API доставку
  base_url: "https://your-server.com"    # URL вашего API сервера
  bearer_token: "your_secret_token"      # Секретный токен для авторизации
  delivery_endpoint: "/api/delivery"     # Endpoint для выдачи товаров
  rollback_endpoint: "/api/rollback"     # Endpoint для отмены выдачи
  timeout: 30                            # Таймаут запроса в секундах

Включение для товаров

В каждом товаре добавьте флаг api_delivery_enabled: true:

items:
  premium_privilege:
    id: "premium_privilege"
    title: "Premium привилегия"
    type: "privilege"
    price_usd: 15.0
    payment_methods: ["lava", "telegram_stars"]
    enabled: true
    api_delivery_enabled: true  # ← Включить API доставку для этого товара

Переопределение для отдельного товара

Если нужны разные настройки для конкретного товара (другой сервер, токен, endpoints):

items:
  special_item:
    id: "special_item"
    title: "Особый товар"
    type: "privilege"
    price_usd: 50.0
    enabled: true
    
    # Переопределяем настройки для этого товара
    api_delivery:
      enabled: true
      base_url: "https://special-server.com"
      bearer_token: "different_token"
      delivery_endpoint: "/api/vip/deliver"
      rollback_endpoint: "/api/vip/rollback"
      timeout: 60

📋 Приоритет настроек:

  1. Собственная конфигурация товара (api_delivery в товаре)
  2. Глобальная конфигурация (api_delivery_enabled: true + глобальная секция)
  3. RCON доставка (если API не настроен)

🌐 Endpoints и "Webhook" в конфиге

⚠️ Важное уточнение про "webhook":

В конфигурации упоминаются delivery_endpoint и rollback_endpoint. Это НЕ webhook для Telegram!

Это endpoints вашего собственного API сервера, на которые бот будет отправлять POST запросы для доставки и отмены товаров.

Что такое endpoints?

Endpoints (эндпоинты) — это пути (URL адреса) на вашем сервере, которые обрабатывают входящие HTTP запросы:

Endpoint Назначение Когда вызывается
delivery_endpoint Выдача товара игроку После успешной оплаты товара
rollback_endpoint Отмена выдачи товара При возврате средств (refund)

Как формируется полный URL?

Бот объединяет base_url + endpoint для получения полного адреса:

base_url = "https://your-server.com"
delivery_endpoint = "/api/delivery"

Полный URL = "https://your-server.com/api/delivery"

💡 Можно ли сделать endpoint для проверки статуса?

Да! Вы можете добавить в свой API сервер дополнительный endpoint для проверки статуса доставки, например /api/delivery/status/{delivery_id}. Бот не использует его автоматически, но вы можете вызывать его вручную для отладки или мониторинга.

📨 Примеры запросов и ответов

Запрос на доставку (Delivery)

POST {base_url}{delivery_endpoint}

Headers:

Authorization: Bearer your_secret_token
Content-Type: application/json

Request Body (JSON):

{
  "delivery_id": 123,                    // Уникальный ID доставки
  "purchase_id": 456,                    // ID покупки в базе данных бота
  "item_id": "premium_privilege",        // ID товара из конфига
  "item_title": "Premium привилегия",    // Название товара
  "minecraft_nick": "Player123",         // Никнейм игрока (указан при покупке)
  "quantity": 1,                         // Количество товара
  "timestamp": "2025-10-27T15:30:00Z"    // Время покупки (ISO 8601)
}

Успешный Response (HTTP 200):

{
  "success": true,
  "delivery_id": 123,
  "message": "Item delivered successfully"
}

✅ Успешная доставка:

Любой HTTP статус код в диапазоне 200-299 считается успехом. Тело ответа может быть любым JSON объектом — бот его не проверяет.

Ошибка Response (HTTP 400/500/etc):

{
  "error": "Player not found",
  "delivery_id": 123
}

❌ Ошибка доставки:

Любой HTTP статус код вне диапазона 200-299 считается ошибкой. Бот автоматически повторит запрос несколько раз (см. Повторные запросы).

Запрос на отмену (Rollback)

POST {base_url}{rollback_endpoint}

Headers:

Authorization: Bearer your_secret_token
Content-Type: application/json

Request Body (JSON):

{
  "delivery_id": 123,
  "purchase_id": 456,
  "item_id": "premium_privilege",
  "item_title": "Premium привилегия",
  "minecraft_nick": "Player123",
  "quantity": 1,
  "timestamp": "2025-10-27T15:30:00Z"
}

Формат запроса идентичен запросу доставки, только отправляется на другой endpoint.

Успешный Response (HTTP 200):

{
  "success": true,
  "delivery_id": 123,
  "message": "Rollback completed successfully"
}

🔁 Логика повторных запросов

Да, бот делает повторные попытки при ошибках!

Если ваш API сервер недоступен или возвращает ошибку, бот автоматически повторяет запрос доставки несколько раз с увеличивающимися задержками:

Попытка Задержка Уведомление пользователю
1 Сразу
2 ~10 секунд "Доставка задерживается..."
3 ~30 секунд "Повторная попытка доставки..."
4 ~1 минута "Доставка все еще выполняется..."
5 ~2 минуты "Последняя попытка доставки..."

После 5-й неудачной попытки бот прекращает попытки и уведомляет администратора о проблеме. Пользователю показывается сообщение о том, что товар будет доставлен вручную.

Когда происходит retry?

  • ❌ Сервер недоступен (timeout, connection refused)
  • ❌ HTTP статус код не 2xx (400, 500, 503, и т.д.)
  • ❌ Неверный формат ответа (не JSON)

Когда retry НЕ происходит?

  • ✅ HTTP 200-299 (успех)

⚠️ Идемпотентность:

Ваш сервер должен быть готов к повторным запросам с одинаковым delivery_id. Используйте это поле для проверки, не была ли доставка уже выполнена ранее.

Пример обработки повторных запросов:

@app.route('/api/delivery', methods=['POST'])
def delivery():
    data = request.json
    delivery_id = data['delivery_id']
    
    # Проверяем, не была ли доставка уже выполнена
    if is_already_delivered(delivery_id):
        return jsonify({"success": True, "message": "Already delivered"}), 200
    
    # Выполняем доставку
    perform_delivery(data)
    
    # Сохраняем факт доставки
    mark_as_delivered(delivery_id)
    
    return jsonify({"success": True}), 200

🔍 Проверка статуса доставки

Хотя бот не использует endpoint для проверки статуса автоматически, вы можете добавить его в свой API для мониторинга и отладки:

Рекомендуемая реализация

GET /api/delivery/status/{delivery_id}

Response:

{
  "delivery_id": 123,
  "status": "completed",           // completed, pending, failed
  "item_id": "premium_privilege",
  "minecraft_nick": "Player123",
  "delivered_at": "2025-10-27T15:30:15Z",
  "attempts": 1,
  "last_error": null
}

Возможные статусы:

  • pending — Доставка в процессе
  • completed — Доставка успешно выполнена
  • failed — Доставка не удалась после всех попыток

💡 Зачем это нужно?

  • Мониторинг доставок в реальном времени
  • Отладка проблем с доставкой
  • Ручная проверка статуса конкретной покупки
  • Построение отчетов и аналитики

💻 Примеры реализации серверов

Python (Flask)

from flask import Flask, request, jsonify
import time

app = Flask(__name__)
BEARER_TOKEN = "your_secret_token"

# База данных доставок (в реальности используйте PostgreSQL/MySQL)
delivered_items = {}

def check_auth():
    auth = request.headers.get('Authorization', '')
    if not auth.startswith('Bearer '):
        return False
    token = auth.split('Bearer ')[1]
    return token == BEARER_TOKEN

@app.route('/api/delivery', methods=['POST'])
def delivery():
    # Проверка авторизации
    if not check_auth():
        return jsonify({"error": "Unauthorized"}), 401
    
    data = request.json
    delivery_id = data.get('delivery_id')
    minecraft_nick = data.get('minecraft_nick')
    item_id = data.get('item_id')
    quantity = data.get('quantity', 1)
    
    # Проверка на повторный запрос (идемпотентность)
    if delivery_id in delivered_items:
        return jsonify({
            "success": True, 
            "message": "Already delivered",
            "delivery_id": delivery_id
        }), 200
    
    # Ваша логика выдачи товара
    print(f"Delivering {item_id} x{quantity} to {minecraft_nick}")
    
    # Например, выполните команду на сервере Minecraft через RCON
    # rcon_client.execute(f"give {minecraft_nick} {item_id} {quantity}")
    
    # Или через плагин API
    # minecraft_api.grant_item(minecraft_nick, item_id, quantity)
    
    # Сохраняем факт доставки
    delivered_items[delivery_id] = {
        "minecraft_nick": minecraft_nick,
        "item_id": item_id,
        "quantity": quantity,
        "timestamp": time.time()
    }
    
    return jsonify({
        "success": True,
        "delivery_id": delivery_id,
        "message": "Item delivered successfully"
    }), 200

@app.route('/api/rollback', methods=['POST'])
def rollback():
    if not check_auth():
        return jsonify({"error": "Unauthorized"}), 401
    
    data = request.json
    delivery_id = data.get('delivery_id')
    minecraft_nick = data.get('minecraft_nick')
    item_id = data.get('item_id')
    quantity = data.get('quantity', 1)
    
    # Ваша логика отмены выдачи
    print(f"Rolling back {item_id} x{quantity} from {minecraft_nick}")
    
    # Например, заберите товар обратно
    # rcon_client.execute(f"clear {minecraft_nick} {item_id} {quantity}")
    
    # Удаляем из базы доставок
    if delivery_id in delivered_items:
        del delivered_items[delivery_id]
    
    return jsonify({
        "success": True,
        "delivery_id": delivery_id,
        "message": "Rollback completed successfully"
    }), 200

@app.route('/api/delivery/status/', methods=['GET'])
def check_status(delivery_id):
    if not check_auth():
        return jsonify({"error": "Unauthorized"}), 401
    
    if delivery_id in delivered_items:
        item = delivered_items[delivery_id]
        return jsonify({
            "delivery_id": delivery_id,
            "status": "completed",
            "minecraft_nick": item["minecraft_nick"],
            "item_id": item["item_id"],
            "quantity": item["quantity"],
            "delivered_at": item["timestamp"]
        }), 200
    else:
        return jsonify({
            "delivery_id": delivery_id,
            "status": "not_found"
        }), 404

if __name__ == '__main__':
    app.run(host='0.0.0.0', port=5000)

Node.js (Express)

const express = require('express');
const app = express();

const BEARER_TOKEN = 'your_secret_token';
const deliveredItems = {};

app.use(express.json());

// Middleware для проверки авторизации
function checkAuth(req, res, next) {
    const auth = req.headers.authorization || '';
    if (!auth.startsWith('Bearer ')) {
        return res.status(401).json({ error: 'Unauthorized' });
    }
    const token = auth.substring(7);
    if (token !== BEARER_TOKEN) {
        return res.status(401).json({ error: 'Invalid token' });
    }
    next();
}

app.post('/api/delivery', checkAuth, (req, res) => {
    const { delivery_id, minecraft_nick, item_id, quantity = 1 } = req.body;
    
    // Проверка на повторный запрос (идемпотентность)
    if (deliveredItems[delivery_id]) {
        return res.json({
            success: true,
            message: 'Already delivered',
            delivery_id
        });
    }
    
    // Ваша логика выдачи товара
    console.log(`Delivering ${item_id} x${quantity} to ${minecraft_nick}`);
    
    // Например, выполните команду через RCON или API
    // minecraftAPI.grantItem(minecraft_nick, item_id, quantity);
    
    // Сохраняем факт доставки
    deliveredItems[delivery_id] = {
        minecraft_nick,
        item_id,
        quantity,
        timestamp: Date.now()
    };
    
    res.json({
        success: true,
        delivery_id,
        message: 'Item delivered successfully'
    });
});

app.post('/api/rollback', checkAuth, (req, res) => {
    const { delivery_id, minecraft_nick, item_id, quantity = 1 } = req.body;
    
    // Ваша логика отмены выдачи
    console.log(`Rolling back ${item_id} x${quantity} from ${minecraft_nick}`);
    
    // Удаляем из базы доставок
    if (deliveredItems[delivery_id]) {
        delete deliveredItems[delivery_id];
    }
    
    res.json({
        success: true,
        delivery_id,
        message: 'Rollback completed successfully'
    });
});

app.get('/api/delivery/status/:delivery_id', checkAuth, (req, res) => {
    const delivery_id = parseInt(req.params.delivery_id);
    
    if (deliveredItems[delivery_id]) {
        const item = deliveredItems[delivery_id];
        return res.json({
            delivery_id,
            status: 'completed',
            minecraft_nick: item.minecraft_nick,
            item_id: item.item_id,
            quantity: item.quantity,
            delivered_at: item.timestamp
        });
    } else {
        return res.status(404).json({
            delivery_id,
            status: 'not_found'
        });
    }
});

app.listen(5000, () => {
    console.log('API server running on port 5000');
});

🔐 Безопасность

⚠️ Критически важные рекомендации:

1. Используйте HTTPS в продакшене

Никогда не используйте HTTP в production окружении! Bearer token передается в открытом виде и может быть перехвачен.

# ❌ Небезопасно
base_url: "http://your-server.com"

# ✅ Безопасно
base_url: "https://your-server.com"

2. Генерируйте сильные токены

Используйте токены длиной минимум 32 символа, содержащие случайные буквы, цифры и специальные символы:

# ❌ Слабый токен
bearer_token: "12345"

# ✅ Сильный токен (пример генерации в Python)
import secrets
token = secrets.token_urlsafe(32)
# Результат: "xK8_2mP9jQ7vN4hR6wL3yS5zT1aB0cD8eF9gH2iJ4k"

3. Храните токены в безопасности

  • ❌ Не коммитьте config.yml с реальными токенами в Git
  • ✅ Используйте переменные окружения или секретные файлы
  • ✅ Добавьте config.yml в .gitignore

4. Валидируйте входящие данные

Всегда проверяйте данные на стороне сервера:

@app.route('/api/delivery', methods=['POST'])
def delivery():
    data = request.json
    
    # Проверка обязательных полей
    required_fields = ['delivery_id', 'item_id', 'minecraft_nick']
    for field in required_fields:
        if field not in data:
            return jsonify({"error": f"Missing field: {field}"}), 400
    
    # Валидация типов
    if not isinstance(data['delivery_id'], int):
        return jsonify({"error": "delivery_id must be integer"}), 400
    
    # Валидация значений
    if not data['minecraft_nick'].isalnum():
        return jsonify({"error": "Invalid minecraft_nick"}), 400
    
    # Продолжаем обработку...

5. Ограничьте rate limiting

Защитите API от DDoS атак и перебора токенов:

from flask_limiter import Limiter
from flask_limiter.util import get_remote_address

limiter = Limiter(
    app=app,
    key_func=get_remote_address,
    default_limits=["200 per hour", "50 per minute"]
)

@app.route('/api/delivery', methods=['POST'])
@limiter.limit("10 per minute")  # Максимум 10 доставок в минуту
def delivery():
    # ...

6. Логируйте все запросы

Сохраняйте логи для аудита и отладки:

  • Timestamp запроса
  • IP адрес клиента
  • Данные о доставке (без чувствительной информации)
  • Результат операции (успех/ошибка)

⚠️ Не логируйте:

  • Bearer токены
  • Пароли или другие секреты
  • Полные данные платежных систем

📚 Дополнительные ресурсы

✨ Готово!

Теперь вы знаете, как работает API доставка товаров. Если у вас остались вопросы, обратитесь к исходным файлам документации или свяжитесь с разработчиком.