Подписки через Stars

Создашь Stars-подписку на 30 дней через createInvoiceLink с subscription_period и отменишь продление через editUserStarSubscription.

Подписки за Stars

С Bot API 8.0 бот может выставлять рекуррентный инвойс в Stars: пользователь платит раз в период, Telegram списывает Stars автоматически при продлении. Сейчас единственный допустимый период — 2592000 секунд (30 дней). Цена подписки — не больше 10000 Stars.

Подписку создают через createInvoiceLink с полем subscription_period (не через обычный sendInvoice для media в чат — для bot subscription используй invoice link / deep link).


Создаём и обслуживаем подписку

typescript
// Telegram Bot API 8.0+ (подписки Stars)
const BOT_TOKEN = process.env.BOT_TOKEN!;
const API = `https://api.telegram.org/bot${BOT_TOKEN}`;
const PERIOD_30_DAYS = 2_592_000; // единственное допустимое значение

async function call<T>(method: string, body: Record<string, unknown>): Promise<T> {
  const res = await fetch(`${API}/${method}`, {
    method: "POST",
    headers: { "Content-Type": "application/json" },
    body: JSON.stringify(body),
  });
  const json = (await res.json()) as { ok: boolean; result: T; description?: string };
  if (!json.ok) throw new Error(`${method}: ${json.description}`);
  return json.result;
}

/** Ссылка на подписку Pro (100 Stars / 30 дней) */
export async function createProSubscriptionLink(userId: number): Promise<string> {
  return call<string>("createInvoiceLink", {
    title: "Pro подписка",
    description: "Доступ к Pro на 30 дней с автопродлением",
    payload: `sub:pro:${userId}`,
    provider_token: "",
    currency: "XTR",
    prices: [{ label: "Pro / 30 дней", amount: 100 }],
    subscription_period: PERIOD_30_DAYS,
  });
}

type SuccessfulPayment = {
  currency: string;
  total_amount: number;
  invoice_payload: string;
  telegram_payment_charge_id: string;
  is_recurring?: true;
  is_first_recurring?: true;
  subscription_expiration_date?: number; // unix seconds
};

export async function onSubscriptionPayment(
  userId: number,
  payment: SuccessfulPayment,
) {
  if (payment.currency !== "XTR") return;

  await upsertSubscription({
    userId,
    plan: "pro",
    chargeId: payment.telegram_payment_charge_id,
    expiresAt: payment.subscription_expiration_date
      ? new Date(payment.subscription_expiration_date * 1000)
      : null,
    isRecurring: Boolean(payment.is_recurring),
    isFirst: Boolean(payment.is_first_recurring),
  });

  // is_first_recurring — первая оплата; последующие продления тоже приходят
  // как successful_payment с is_recurring
  await enableProFeatures(userId);
}

/** Отменить автопродление (доступ остаётся до конца оплаченного периода) */
export async function cancelSubscriptionRenewal(
  userId: number,
  telegramPaymentChargeId: string,
) {
  return call<boolean>("editUserStarSubscription", {
    user_id: userId,
    telegram_payment_charge_id: telegramPaymentChargeId,
    is_canceled: true,
  });
}

/** Разрешить пользователю снова включить продление */
export async function allowSubscriptionRenewal(
  userId: number,
  telegramPaymentChargeId: string,
) {
  return call<boolean>("editUserStarSubscription", {
    user_id: userId,
    telegram_payment_charge_id: telegramPaymentChargeId,
    is_canceled: false,
  });
}

declare function upsertSubscription(row: {
  userId: number;
  plan: string;
  chargeId: string;
  expiresAt: Date | null;
  isRecurring: boolean;
  isFirst: boolean;
}): Promise<void>;
declare function enableProFeatures(userId: number): Promise<void>;

Для платного канала отдельно существует createChatSubscriptionInviteLink (subscription_period: 2592000, subscription_price 1–10000) — это доступ к каналу, не bot-feature subscription. Не смешивай модели в одной таблице без поля kind.

Как это работает

  • subscription_period: 2592000 — включает режим подписки; валюта обязана быть XTR.
  • У одного пользователя может быть несколько активных подписок на одного бота одновременно.
  • В successful_payment смотри is_recurring, is_first_recurring, subscription_expiration_date.
  • editUserStarSubscription(is_canceled: true) — останавливает продление, не «мгновенный бан»; до subscription_expiration_date доступ обычно сохраняется (логика продукта — на тебе).
  • Pre-checkout для подписок тот же: отвечай ≤10 с и валидируй payload/сумму.

Частые ошибки

Ставишь period ≠ 2592000 → Bot API отклонит запрос. Других периодов пока нет.

Ждёшь списания как on-chain invoice в Gram → продление списывает Stars с баланса пользователя в Telegram, это не Gram-транзакция и не TON Connect.

Хранить только первый charge_id и игнорировать продления → каждое успешное продление нужно учитывать (срок, статус, поддержка /paysupport).


Что дальше

Материалы gramdocs.tech носят образовательный характер и не являются финансовой, юридической или инвестиционной рекомендацией. Работа с блокчейном TON и токеном Gram связана с рисками потери средств. Правовая информация