Вернёшь 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), актуально на июль 2026const 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 невозможен.
Как это работает
refundStarPayment → true при успехе; 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 связана с рисками потери средств. Правовая информация