Возврат платежа (refund) в Stars

Вернёшь Stars пользователю через refundStarPayment, сохранив идемпотентность и отзывая выданный доступ.

Refund Stars

Возврат цифрового платежа Stars делается методом refundStarPayment: нужны user_id плательщика и telegram_payment_charge_id из successful_payment. После успеха Stars возвращаются пользователю; ты обязан отозвать товар/доступ в своей системе.

Бот обязан уметь обрабатывать споры (команда /paysupport по правилам Telegram). Refund — основной программный инструмент для этого.


Возврат и откат доступа

typescript
// Telegram Bot API 7.4+ (refundStarPayment), актуально на июль 2026
const BOT_TOKEN = process.env.BOT_TOKEN!;
const API = `https://api.telegram.org/bot${BOT_TOKEN}`;

type PaymentRow = {
  userId: number;
  chargeId: string;
  payload: string;
  stars: number;
  refundedAt: Date | null;
};

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;
}

export async function refundStarsPayment(chargeId: string): Promise<void> {
  const payment = await findPaymentByChargeId(chargeId);
  if (!payment) throw new Error("Платёж не найден в БД");
  if (payment.refundedAt) return; // уже возвращён у нас

  // 1. Возврат в Telegram
  await call<boolean>("refundStarPayment", {
    user_id: payment.userId,
    telegram_payment_charge_id: payment.chargeId,
  });

  // 2. Фиксируем у себя (идемпотентно)
  await markRefunded(payment.chargeId);

  // 3. Отзываем товар / подписку
  await revokeEntitlement(payment.userId, payment.payload);

  await call("sendMessage", {
    chat_id: payment.userId,
    text: `Возврат ${payment.stars} Stars выполнен. Доступ отключён.`,
  });
}

/** Пример хендлера поддержки */
export async function handlePaySupport(userId: number, chargeIdFromUser?: string) {
  if (!chargeIdFromUser) {
    const list = await listRecentPayments(userId, 5);
    await call("sendMessage", {
      chat_id: userId,
      text:
        "Пришли charge_id платежа для возврата.\nПоследние платежи:\n" +
        list.map((p) => `• ${p.chargeId} (${p.stars} Stars)`).join("\n"),
    });
    return;
  }
  await refundStarsPayment(chargeIdFromUser);
}

declare function findPaymentByChargeId(id: string): Promise<PaymentRow | null>;
declare function markRefunded(chargeId: string): Promise<void>;
declare function revokeEntitlement(userId: number, payload: string): Promise<void>;
declare function listRecentPayments(userId: number, limit: number): Promise<PaymentRow[]>;

Сохраняй telegram_payment_charge_id сразу при successful_payment — без него программный refund невозможен.

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

  • refundStarPaymenttrue при успехе; Stars возвращаются на баланс пользователя.
  • Повторный вызов с тем же charge_id обычно завершится ошибкой API — поэтому сначала проверяй refundedAt у себя.
  • Refund не заменяет юридическую/процедурную часть споров: отвечай в /paysupport в разумный срок (см. Bot Platform Developer Terms).
  • Для подписок возврат конкретного списания ≠ editUserStarSubscription: отмена продления и refund — разные операции.

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

Не сохранил telegram_payment_charge_id → refund через Bot API сделать нельзя. Логируй charge id до выдачи товара.

Вернул Stars, но оставил Pro-доступ → пользователь получает и деньги, и товар. revokeEntitlement должен быть в том же сценарии (или компенсирующей джобой при сбое).

Путаешь provider_payment_charge_id и telegram_payment_charge_id → в refundStarPayment нужен именно telegram charge id.


Что дальше

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