🚀 PRocket API v2

PRocket API

Монетизация Telegram-ботов: обязательная подписка, задания и показы. Подключение занимает пять строк — текст, клавиатуру и кнопку «Проверить» отдаёт сервер уже собранными.

Шаг 1. Получите ключ

Откройте мини-апп PRocket → раздел ИнтеграцияПодключить бота. После подключения вы увидите ключ вида prk_live_… — он даёт право действовать от имени бота.

🔑
Токен от BotFather не передаётся в API никогда.

Ни в одном запросе нет поля, которое его принимает. Токен нужен только при подключении бота в мини-аппе — там он и остаётся, зашифрованным.

Шаг 2. Установите библиотеку

pip install procketapi
npm install procketapi
# Библиотека не нужна — API это обычный POST с JSON

Шаг 3. Три строки в хендлер

from aiogram import Bot, Dispatcher, types
from procketapi import PRocket

bot = Bot("ТОКЕН_БОТА")
dp = Dispatcher()
procket = PRocket("ВАШ_КЛЮЧ")


@dp.message()
async def handler(message: types.Message):
    # Не подписан — клиент сам отправит спонсоров и вернёт False
    if not await procket.check(message.from_user.id, bot=bot):
        return

    await message.answer("Доступ открыт")
import { Bot } from "grammy";
import { PRocket } from "procketapi";

const bot = new Bot("ТОКЕН_БОТА");
const procket = new PRocket("ВАШ_КЛЮЧ");

bot.on("message", async (ctx, next) => {
  // Не подписан — клиент сам отправит спонсоров
  const result = await procket.check(ctx.from.id, { bot });
  if (!result.passed) return;

  await next();
});

bot.command("start", (ctx) => ctx.reply("Доступ открыт"));
bot.start();
curl -X POST https://app.procket.club/api/v2/check \
  -H "Content-Type: application/json" \
  -H "Auth: ВАШ_КЛЮЧ" \
  -d '{"user_id": 123456789, "language_code": "ru"}'
Всё.

Дальше — только если нужны задания, показы или своё оформление.

Аутентификация

Все запросы — POST с телом JSON. Ключ можно передать любым из способов ниже: они равнозначны, выбирайте удобный.

Auth: ВАШ_КЛЮЧ
Authorization: Bearer ВАШ_КЛЮЧ
Authorization: ВАШ_КЛЮЧ
X-Procket-Key: ВАШ_КЛЮЧ
X-Api-Key: ВАШ_КЛЮЧ
{
  "key": "ВАШ_КЛЮЧ",
  "user_id": 123456789
}
💡
Почему так много вариантов.

Чтобы код, написанный по документации другого сервиса, заработал после замены одного домена. Разбор ключа разрешающий, проверка — нет: неверный ключ всё равно даёт 401.

Базовый адрес

https://app.procket.club

Каждый метод доступен по трём путям сразу: /api/v2/check, /api/check и просто /check. Выбирайте любой.

Обязательная подписка

Пользователь не попадает в бота, пока не подпишется на выданных спонсоров. Владелец получает оплату за каждую подписку.

POST /api/v2/check также: /check · /subscription · /offers

Запрос

ПолеТипОписание
user_idintegerобяз.Telegram ID пользователя. Принимаются также chat_id и tg_user_id.
limitintegerСколько спонсоров выдать. 1–5, по умолчанию 3.
language_codestringЯзык пользователя. Определяет язык готового сообщения.
is_premiumbooleanTelegram Premium. Влияет на подбор кампаний.
messageobjectСвоё оформление, см. ниже.

Ответ

{
  "passed": false,
  "skip": false,
  "status": "not_ok",
  "reason": "pending",
  "description": "sponsors pending subscription",
  "offers": [
    {
      "id": "cmp_a1b2",
      "ticket": "atc_x9y8",
      "type": "channel",
      "title": "Канал о крипте",
      "url": "https://t.me/example",
      "photo": null,
      "button": "Перейти в канал",
      "reward": 1.5,
      "currency": "USD",
      "done": false,
      "state": "open"
    }
  ],
  "message": {
    "text": "Для доступа к боту подпишитесь на спонсоров:",
    "parse_mode": "HTML",
    "reply_markup": {
      "inline_keyboard": [
        [{ "text": "Подписаться", "url": "https://t.me/example" }],
        [{ "text": "Проверить ✅", "callback_data": "procket_check" }]
      ]
    }
  },
  "attached_until": "2026-08-05T13:00:00.000Z"
}
ПолеОписание
passedГлавное поле. true — пускать дальше, false — показать message.
skip / statusТо же самое под именами из других сервисов. Всегда согласованы с passed.
reasonpending, completed (подписан на всё) или no_offers (нет подходящих кампаний).
offersСпонсоры. Собирать клавиатуру вручную нужно, только если не устраивает message.
messageГотовое сообщение. Отправляется в Telegram как есть. null, когда passed: true.
attached_untilДо какого момента спонсоры закреплены за пользователем.
⚠️
Отдельного метода проверки нет — повторный вызов check и есть проверка.

Именно его вешают на кнопку «Проверить». Сервер сам перепроверит членство по уже выданным спонсорам и вернёт passed: true, когда всё выполнено.

🧮
Повторный вызов возвращает тех же спонсоров, а не новых.

Привязка живёт час (attached_until). Это не ошибка: пользователь их уже видит. Не вызывайте check в цикле ради «обновления выдачи» — так вы завысите показы и занизите конверсию, и заметно это станет через недели, по неправильным цифрам.

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

import asyncio
from aiogram import Bot, Dispatcher, F, types
from aiogram.filters import Command
from procketapi import PRocket

bot = Bot("ТОКЕН_БОТА")
dp = Dispatcher()
procket = PRocket("ВАШ_КЛЮЧ")


@dp.message(Command("start"))
async def start(message: types.Message):
    if not await procket.check(
        message.from_user.id,
        bot=bot,
        language_code=message.from_user.language_code,
        is_premium=message.from_user.is_premium,
    ):
        return

    await message.answer("Доступ открыт")


@dp.callback_query(F.data == "procket_check")
async def on_check(callback: types.CallbackQuery):
    await callback.answer()

    # Повторный вызов и есть проверка
    if await procket.check(callback.from_user.id, bot=bot):
        await callback.message.answer("Спасибо! Доступ открыт.")
    else:
        await callback.message.answer("Вы подписались не на всех спонсоров.")


asyncio.run(dp.start_polling(bot))
import TelegramBot from "node-telegram-bot-api";
import { PRocket } from "procketapi";

const bot = new TelegramBot("ТОКЕН_БОТА", { polling: true });
const procket = new PRocket("ВАШ_КЛЮЧ");

bot.onText(/\/start/, async (msg) => {
  const result = await procket.check(msg.from.id, {
    bot,
    languageCode: msg.from.language_code,
    isPremium: msg.from.is_premium
  });
  if (!result.passed) return;   // спонсоры уже отправлены

  await bot.sendMessage(msg.chat.id, "Доступ открыт");
});

bot.on("callback_query", async (query) => {
  if (query.data !== "procket_check") return;
  await bot.answerCallbackQuery(query.id);

  // Повторный вызов и есть проверка
  const result = await procket.check(query.from.id, { bot });
  await bot.sendMessage(
    query.message.chat.id,
    result.passed ? "Спасибо! Доступ открыт." : "Вы подписались не на всех спонсоров."
  );
});
<?php

function procketCheck(int $userId, ?string $lang = null): array
{
    $ch = curl_init('https://app.procket.club/api/v2/check');

    curl_setopt_array($ch, [
        CURLOPT_POST           => true,
        CURLOPT_RETURNTRANSFER => true,
        CURLOPT_TIMEOUT        => 10,
        CURLOPT_HTTPHEADER     => [
            'Content-Type: application/json',
            'Auth: ' . PROCKET_KEY,
        ],
        CURLOPT_POSTFIELDS => json_encode([
            'user_id'       => $userId,
            'language_code' => $lang,
        ]),
    ]);

    $response = curl_exec($ch);
    curl_close($ch);

    // Сервис недоступен — пропускаем: бот важнее офферов
    if ($response === false) {
        return ['passed' => true];
    }

    return json_decode($response, true) ?: ['passed' => true];
}

$result = procketCheck($userId, 'ru');

if (!$result['passed']) {
    // message отправляется в Telegram как есть
    sendTelegram('sendMessage', array_merge(
        ['chat_id' => $userId],
        $result['message']
    ));
    return;
}
package procket

import (
	"bytes"
	"encoding/json"
	"net/http"
	"time"
)

type Offer struct {
	ID       string  `json:"id"`
	Ticket   string  `json:"ticket"`
	Title    string  `json:"title"`
	URL      string  `json:"url"`
	Reward   float64 `json:"reward"`
	Currency string  `json:"currency"`
}

type Message struct {
	Text        string          `json:"text"`
	ParseMode   string          `json:"parse_mode"`
	ReplyMarkup json.RawMessage `json:"reply_markup"`
}

type CheckResult struct {
	Passed  bool     `json:"passed"`
	Reason  string   `json:"reason"`
	Offers  []Offer  `json:"offers"`
	Message *Message `json:"message"`
}

var client = &http.Client{Timeout: 10 * time.Second}

func Check(key string, userID int64, lang string) (*CheckResult, error) {
	body, _ := json.Marshal(map[string]any{
		"user_id":       userID,
		"language_code": lang,
	})

	req, err := http.NewRequest("POST", "https://app.procket.club/api/v2/check", bytes.NewReader(body))
	if err != nil {
		return nil, err
	}
	req.Header.Set("Content-Type", "application/json")
	req.Header.Set("Auth", key)

	resp, err := client.Do(req)
	if err != nil {
		// Сервис недоступен — пропускаем: бот важнее офферов
		return &CheckResult{Passed: true, Reason: "unavailable"}, nil
	}
	defer resp.Body.Close()

	var result CheckResult
	if err := json.NewDecoder(resp.Body).Decode(&result); err != nil {
		return nil, err
	}
	return &result, nil
}
using System.Net.Http.Json;
using System.Text.Json.Serialization;

public record Offer(
    [property: JsonPropertyName("id")] string Id,
    [property: JsonPropertyName("ticket")] string Ticket,
    [property: JsonPropertyName("title")] string Title,
    [property: JsonPropertyName("url")] string Url,
    [property: JsonPropertyName("reward")] decimal Reward);

public record CheckResult(
    [property: JsonPropertyName("passed")] bool Passed,
    [property: JsonPropertyName("reason")] string Reason,
    [property: JsonPropertyName("offers")] Offer[] Offers);

public sealed class PRocketClient
{
    private static readonly HttpClient Http = new()
    {
        BaseAddress = new Uri("https://app.procket.club"),
        Timeout = TimeSpan.FromSeconds(10)
    };

    private readonly string _key;

    public PRocketClient(string key) => _key = key;

    public async Task<CheckResult> CheckAsync(long userId, string? lang = null)
    {
        using var request = new HttpRequestMessage(HttpMethod.Post, "/api/v2/check")
        {
            Content = JsonContent.Create(new { user_id = userId, language_code = lang })
        };
        request.Headers.Add("Auth", _key);

        try
        {
            var response = await Http.SendAsync(request);
            return await response.Content.ReadFromJsonAsync<CheckResult>()
                   ?? new CheckResult(true, "unavailable", []);
        }
        catch (HttpRequestException)
        {
            // Сервис недоступен — пропускаем: бот важнее офферов
            return new CheckResult(true, "unavailable", []);
        }
    }
}
curl -X POST https://app.procket.club/api/v2/check \
  -H "Content-Type: application/json" \
  -H "Auth: ВАШ_КЛЮЧ" \
  -d '{
    "user_id": 123456789,
    "limit": 3,
    "language_code": "ru",
    "is_premium": false
  }'

Задания

То же самое, но пользователь получает награду, а не доступ. Начисляете её вы — сервер лишь подтверждает, что задание выполнено.

POST /api/v2/tasks также: /tasks · /get_tasks

Запрос

ПолеТипОписание
user_idintegerобяз.Telegram ID пользователя.
limitintegerСколько заданий выдать. 1–10, по умолчанию 5.
language_codestringЯзык пользователя.
is_premiumbooleanTelegram Premium.

Ответ

{
  "tasks": [
    {
      "id": "cmp_a1b2",
      "ticket": "atc_x9y8",
      "type": "channel",
      "title": "Канал о крипте",
      "url": "https://t.me/example",
      "button": "Перейти в канал",
      "reward": 1.5,
      "currency": "USD",
      "done": false,
      "state": "open"
    }
  ],
  "completed": false,
  "skip": false,
  "message": { "text": "…", "parse_mode": "HTML", "reply_markup": { } },
  "attached_until": "2026-08-05T13:00:00.000Z"
}

Проверка одного задания

POST /api/v2/check_task также: /check_task · /tasks/check

Передаётся ticket из задания — идентификатор привязки к пользователю, не id кампании.

{
  "state": "done",
  "result": "done",
  "completed": true,
  "reward": 1.5,
  "currency": "USD"
}
stateЗначение
openвыдано, ещё не выполнено
doneвыполнено — начисляйте награду
waitingвыполнено, идёт удержание
paidоплачено владельцу
expiredпривязка истекла
cancelledснято (кампания недоступна для проверки)
revertedпользователь отписался, начисление отменено
💸
Ведите учёт оплаченных тикетов у себя.

Один и тот же ticket будет возвращать done до истечения привязки. Без своей проверки награда начислится столько раз, сколько пользователь нажмёт кнопку.

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

from aiogram import F, types
from aiogram.filters import Command
from procketapi import PRocket

procket = PRocket("ВАШ_КЛЮЧ")


@dp.message(Command("tasks"))
async def show_tasks(message: types.Message):
    result = await procket.get_tasks(message.from_user.id, limit=5)

    if not result:
        await message.answer("Сейчас заданий нет. Загляните позже.")
        return

    # Готовое сообщение — клавиатуру верстать не нужно
    await message.answer(**result.message.as_kwargs())


@dp.callback_query(F.data == "procket_check")
async def check_tasks(callback: types.CallbackQuery):
    await callback.answer()

    result = await procket.get_tasks(callback.from_user.id)
    earned = 0.0

    for task in result:
        state = await procket.check_task(task.ticket)

        # already_paid — ваша таблица; без неё награда начислится дважды
        if state and not await already_paid(task.ticket):
            await mark_paid(task.ticket)
            earned += state.reward

    if earned:
        await callback.message.answer(f"Начислено: {earned:.2f}")
    else:
        await callback.message.answer("Задания ещё не выполнены.")
import { PRocket } from "procketapi";

const procket = new PRocket("ВАШ_КЛЮЧ");

bot.command("tasks", async (ctx) => {
  const { tasks, message } = await procket.getTasks(ctx.from.id, { limit: 5 });

  if (!tasks.length) {
    return ctx.reply("Сейчас заданий нет. Загляните позже.");
  }

  // Готовое сообщение — клавиатуру верстать не нужно
  await ctx.reply(message.text, {
    parse_mode: message.parse_mode,
    reply_markup: message.reply_markup
  });
});

bot.callbackQuery("procket_check", async (ctx) => {
  await ctx.answerCallbackQuery();

  const { tasks } = await procket.getTasks(ctx.from.id);
  let earned = 0;

  for (const task of tasks) {
    const state = await procket.checkTask(task.ticket);

    // alreadyPaid — ваша таблица; без неё награда начислится дважды
    if (state.completed && !(await alreadyPaid(task.ticket))) {
      await markPaid(task.ticket);
      earned += state.reward;
    }
  }

  await ctx.reply(earned ? `Начислено: ${earned.toFixed(2)}` : "Задания ещё не выполнены.");
});
# Выдать задания
curl -X POST https://app.procket.club/api/v2/tasks \
  -H "Content-Type: application/json" \
  -H "Auth: ВАШ_КЛЮЧ" \
  -d '{"user_id": 123456789, "limit": 5}'

# Проверить одно задание
curl -X POST https://app.procket.club/api/v2/check_task \
  -H "Content-Type: application/json" \
  -H "Auth: ВАШ_КЛЮЧ" \
  -d '{"ticket": "atc_x9y8"}'

Показы и приветы

Рекламный пост в личку пользователю. Отправляет его сервер через токен вашего бота — вам не нужно ничего верстать и пересылать.

POST /api/v2/ad/send также: /ad/send · /views · /hi

Запрос

ПолеТипОписание
user_idintegerобяз.Кому отправить. Принимается также SendToChatId.
hibooleanРежим привета: только для новых пользователей, не чаще раза в сутки.
language_codestringЯзык пользователя.

Коды результата

КодЗначениеЧто делать
1Пост доставленничего
2Токен бота недействителенпереподключить бота в мини-аппе
3Пользователь заблокировал ботапометить у себя, больше не слать
4Превышен лимитповторить позже
5Ошибка Telegramповторить позже
6Внутренняя ошибкаповторить позже
7Лимит показов исчерпанштатно, ничего
8Нет подходящей рекламыштатно, ничего
9Бот выключен в настройкахвключить в мини-аппе
10Бот заблокированобратиться в поддержку
11Бот на модерациидождаться проверки
🚫
Правила вызова. Их нарушение — повод отключить бота от сервиса.

hi: true — только сразу после /start нового пользователя и не чаще раза в сутки на человека.

Обычные показы — только после осмысленного действия пользователя, а не на каждое сообщение и не по таймеру.

Не вызывайте метод без входящего апдейта и не подставляйте чужие user_id.

Пример

from aiogram.filters import Command
from procketapi import AdResult, PRocket

procket = PRocket("ВАШ_КЛЮЧ")


@dp.message(Command("start"))
async def start(message: types.Message):
    # Привет: только для новых пользователей, первым делом после /start
    if await is_new_user(message.from_user.id):
        await procket.send_ad(message.from_user.id, hi=True)

    await message.answer("Привет! Я умею…")


@dp.message(Command("profile"))
async def profile(message: types.Message):
    await message.answer(render_profile(message.from_user.id))

    # Обычный показ — после осмысленного действия, а не на /start
    result = await procket.send_ad(message.from_user.id)

    if result.code == AdResult.USER_FORBIDDEN:
        await mark_blocked(message.from_user.id)
import { AdResult, PRocket } from "procketapi";

const procket = new PRocket("ВАШ_КЛЮЧ");

bot.command("start", async (ctx) => {
  // Привет: только для новых пользователей, первым делом после /start
  if (await isNewUser(ctx.from.id)) {
    await procket.sendAd(ctx.from.id, { hi: true });
  }

  await ctx.reply("Привет! Я умею…");
});

bot.command("profile", async (ctx) => {
  await ctx.reply(renderProfile(ctx.from.id));

  // Обычный показ — после осмысленного действия, а не на /start
  const result = await procket.sendAd(ctx.from.id);

  if (result.code === AdResult.USER_FORBIDDEN) {
    await markBlocked(ctx.from.id);
  }
});
curl -X POST https://app.procket.club/api/v2/ad/send \
  -H "Content-Type: application/json" \
  -H "Auth: ВАШ_КЛЮЧ" \
  -d '{"user_id": 123456789, "hi": true}'

# {"SendPostResult": 1, "result": 1, "ok": true,
#  "description": "Advertisement delivered"}

Вебхуки

Сервер сообщит, когда пользователь выполнил подписку или задание — не нужно опрашивать API в цикле. Адрес задаётся в карточке интеграции.

Тело события

{
  "id": "evt_7f3a",
  "type": "task.completed",
  "created_at": "2026-08-05T12:00:00.000Z",
  "bot_id": "bot_a1b2",
  "data": {
    "ticket": "atc_x9y8",
    "user_id": 123456789,
    "reward": 1.5
  }
}
СобытиеКогда
subscription.completedпользователь подписался на спонсора в режиме ОП
task.completedпользователь выполнил задание

Заголовки

ЗаголовокЗначение
X-Procket-Eventтип события
X-Procket-Deliveryидентификатор доставки
X-Procket-Signaturet=<unix>,v1=<hex>

Проверка подписи

Подпись — HMAC-SHA256 по строке <t>.<тело> секретом вебхука.

🔐
Проверять обязательно.

Без этого любой, кто узнал адрес вашего обработчика, пришлёт поддельное «задание выполнено» — и вы начислите за него награду.

Считайте подпись по исходным байтам тела. Разбор JSON с обратной сборкой её ломает: порядок ключей не сохраняется.

Запросы старше 5 минут отвергайте, иначе перехваченный запрос переигрывается.

import json
from aiohttp import web
from procketapi import WebhookSignatureError, verify_webhook

SECRET = "СЕКРЕТ_ИЗ_КАРТОЧКИ_ИНТЕГРАЦИИ"


async def handle(request: web.Request):
    # Именно байты, не разобранный JSON
    body = await request.read()

    try:
        verify_webhook(body, request.headers.get("X-Procket-Signature"), SECRET)
    except WebhookSignatureError:
        return web.Response(status=403)

    event = json.loads(body)
    data = event["data"]

    if event["type"] == "task.completed":
        # Повтор доставки — штатная ситуация, проверьте ticket у себя
        if not await already_paid(data["ticket"]):
            await give_reward(data["user_id"], data["reward"])
            await mark_paid(data["ticket"])

    # 2xx = доставлено
    return web.json_response({"ok": True})


app = web.Application()
app.router.add_post("/procket/webhook", handle)
web.run_app(app, port=8080)
import express from "express";
import { verifyWebhook, WebhookSignatureError } from "procketapi";

const app = express();

// express.raw, не express.json: подпись считается по исходным байтам
app.post(
  "/procket/webhook",
  express.raw({ type: "application/json" }),
  async (req, res) => {
    try {
      verifyWebhook(req.body, req.get("X-Procket-Signature"), process.env.PROCKET_SECRET);
    } catch (error) {
      if (error instanceof WebhookSignatureError) return res.sendStatus(403);
      throw error;
    }

    const event = JSON.parse(req.body.toString("utf8"));
    const data = event.data;

    if (event.type === "task.completed") {
      // Повтор доставки — штатная ситуация, проверьте ticket у себя
      if (!(await alreadyPaid(data.ticket))) {
        await giveReward(data.user_id, data.reward);
        await markPaid(data.ticket);
      }
    }

    res.json({ ok: true });   // 2xx = доставлено
  }
);

app.listen(8080);
<?php

$secret = getenv('PROCKET_SECRET');
$body   = file_get_contents('php://input');   // исходные байты
$header = $_SERVER['HTTP_X_PROCKET_SIGNATURE'] ?? '';

parse_str(strtr($header, ',', '&'), $parts);
$timestamp = $parts['t'] ?? '';
$provided  = $parts['v1'] ?? '';

// Запросы старше 5 минут отвергаем: иначе перехваченный переигрывается
if (!$timestamp || abs(time() - (int) $timestamp) > 300) {
    http_response_code(403);
    exit;
}

$expected = hash_hmac('sha256', $timestamp . '.' . $body, $secret);

// hash_equals, а не ===: сравнение строк выдаёт подпись по времени ответа
if (!hash_equals($expected, $provided)) {
    http_response_code(403);
    exit;
}

$event = json_decode($body, true);

if ($event['type'] === 'task.completed') {
    $data = $event['data'];
    // Повтор доставки — штатная ситуация, проверьте ticket у себя
    if (!alreadyPaid($data['ticket'])) {
        giveReward($data['user_id'], $data['reward']);
        markPaid($data['ticket']);
    }
}

header('Content-Type: application/json');
echo json_encode(['ok' => true]);   // 2xx = доставлено

Повторы

Ответ 2xx считается доставкой. Иначе повторы через 10 с, 60 с, 5 мин, 30 мин и 2 ч, после чего доставка признаётся неудавшейся. Пять подряд неудавшихся доставок отключают вебхук.

Библиотеки

Официальные клиенты берут на себя ретраи, таймауты и отправку сообщений.

Middleware: подписка на весь бот одной строкой

from procketapi import PRocket, PRocketMiddleware

procket = PRocket("ВАШ_КЛЮЧ")

# skip_commands — то, что должно работать без подписки
dp.message.middleware(PRocketMiddleware(procket, skip_commands=("/help", "/support")))
dp.callback_query.middleware(PRocketMiddleware(procket))

# Ни один хендлер ниже не выполнится, пока пользователь не подписан.
# Нажатия на кнопку «Проверить» обрабатываются внутри middleware.
import { PRocket } from "procketapi";

const procket = new PRocket("ВАШ_КЛЮЧ");

bot.use(async (ctx, next) => {
  if (!ctx.from) return next();

  // /help должен работать без подписки
  if (ctx.message?.text?.startsWith("/help")) return next();

  const result = await procket.check(ctx.from.id, {
    bot,
    languageCode: ctx.from.language_code
  });
  if (!result.passed) return;

  return next();
});

Поведение при сбоях

🛟
Клиент не имеет права уронить бота.

Если PRocket недоступен, check() возвращает «пропустить»: пользователь продолжает пользоваться ботом, а владелец теряет один показ вместо всех сразу. Офферы важнее нуля, но бот важнее офферов.

Строгое поведение включается через raise_on_error=True (Python) или throwOnError: true (JS).

Своё оформление

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

ПолеТипОписание
textstringТекст сообщения. HTML-разметка Telegram.
rowsintegerКнопок в ряд, 1–4. По умолчанию 1.
button_channelstringПодпись кнопки канала.
button_botstringПодпись кнопки бота.
button_urlstringПодпись кнопки ссылки.
button_checkstringПодпись кнопки «Проверить».
check_callbackstringcallback_data кнопки проверки. По умолчанию procket_check.

Подстановки в text

ПодстановкаЗначение
$name / {name}имя пользователя из user_name
{count}сколько спонсоров выдано
{reward}суммарная награда
{currency}валюта награды
await procket.check(
    user_id,
    bot=bot,
    user_name=message.from_user.first_name,
    message={
        "rows": 2,
        "text": "<b>$name</b>, подпишитесь на {count} канала — и бот ваш:",
        "button_channel": "Подписаться",
        "button_check": "Готово ✅",
    },
)
await procket.check(userId, {
  bot,
  userName: ctx.from.first_name,
  message: {
    rows: 2,
    text: "<b>$name</b>, подпишитесь на {count} канала — и бот ваш:",
    button_channel: "Подписаться",
    button_check: "Готово ✅"
  }
});
curl -X POST https://app.procket.club/api/v2/check \
  -H "Content-Type: application/json" \
  -H "Auth: ВАШ_КЛЮЧ" \
  -d '{
    "user_id": 123456789,
    "user_name": "Иван",
    "message": {
      "rows": 2,
      "text": "$name, подпишитесь на {count} канала:",
      "button_channel": "Подписаться",
      "button_check": "Готово ✅"
    }
  }'

Справочник API

Все методы — POST с телом JSON. Каждый доступен по любому из псевдонимов.

МетодПсевдонимыНазначение
/api/v2/check/check, /subscription, /offers, /subОбязательная подписка
/api/v2/tasks/tasks, /get_tasksВыдать задания
/api/v2/check_task/check_task, /tasks/check, /task_statusСостояние задания
/api/v2/ad/send/ad/send, /ad/SendPost, /views, /hiПоказ или привет
/api/v2/reset/reset, /reset_offersЗакрыть привязки пользователя
/api/v2/me/me, /get_me, /infoБот, баланс, вебхук, лимиты

Объект оффера

ПолеТипОписание
idstringИдентификатор кампании.
ticketstringПривязка оффера к пользователю. Именно он идёт в check_task, не id.
typestringchannel, bot, group или link.
titlestringНазвание ресурса.
urlstringСсылка для кнопки.
photostring | nullАватар ресурса, если есть.
buttonstringРекомендованная подпись кнопки.
rewardnumberВаша выплата за выполнение.
currencystringВалюта выплаты.
donebooleanВыполнен ли оффер.
statestringСм. таблицу состояний в разделе «Задания».

/api/v2/me

{
  "bot": {
    "id": "bot_a1b2",
    "username": "example_bot",
    "title": "Мой бот",
    "status": "active"
  },
  "status": true,
  "balance": 12.5,
  "currency": "USD",
  "webhook": { "url": "https://…", "events": [], "state": "active" },
  "limits": { "attachment_ttl_seconds": 3600, "tasks_max_limit": 10 }
}

webhook равен null, пока адрес не задан. Секреты не возвращаются никогда.

Ошибки и лимиты

КодПричинаЧто делать клиенту
200обработать тело
401ключ не принятостановиться навсегда, одна строка в лог
404неизвестный тикет или опечатка в адресепроверить ticket и путь
422некорректный user_idдефект клиента, исправить
429превышен лимитподождать Retry-After секунд
5xxсбой на нашей сторонедо 3 попыток, пауза 1 с с удвоением
🛑
На 401 нужно останавливаться, а не повторять.

Ретраи не сделают ключ валидным, а лог владельца засорят. Официальные библиотеки делают это сами: после первого 401 клиент замолкает до перезапуска.

Лимиты

ЛимитЗначение
Запросов в минуту на бота60
Время жизни привязки3600 с (1 час)
Спонсоров за раз (ОП)1–5, по умолчанию 3
Заданий за раз1–10, по умолчанию 5
Приветов на пользователя1 в сутки

Тело ошибки

{
  "error": "unauthorized",
  "message": "Key not recognised. Check that you copied the whole key from Integration in the Mini App.",
  "docs": "https://procketapi.best"
}

Переход с других сервисов

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

БылоСталоКомментарий
Auth: KEYработает как естьзаголовок принимается
Authorization: KEYработает как естьбез Bearer тоже
{"key": "…"} в телеработает как естьключ в теле принимается
chat_id, tg_user_id, SendToChatIduser_idвсе три имени принимаются
lang, user_localelanguage_codeпринимаются
signatureticketпринимается
POST /get_tasksPOST /get_tasksпуть совпадает
POST /checkPOST /checkпуть совпадает
POST /ad/SendPostPOST /ad/sendстарое написание принимается

Что поменялось по смыслу

  • skip и status сохранены, но главное поле — passed.
  • Вместо массива ссылок приходит offers с объектами и готовый message. Строить клавиатуру вручную больше не нужно.
  • Проверка выполнения — это повторный check, отдельного метода нет.
  • В check_task передаётся ticket (привязка), а не идентификатор кампании.
📦
Простейшая миграция.

Замените адрес на https://app.procket.club, подставьте свой ключ — и проверьте, что читаете passed. Остальное совпадёт.