Инвойсы через Bot API

Соберёшь Stars-инвойс через sendInvoice и createInvoiceLink, обработаешь pre_checkout и successful_payment без типичных ошибок.

Инвойсы Stars в Bot API

Инвойс — сообщение (или ссылка) с кнопкой Pay. Для цифровых товаров: currency: "XTR", пустой provider_token, в prices ровно один элемент. Дальше — тот же конвейер: pre_checkout_queryanswerPreCheckoutQuerysuccessful_payment.


typescript
// Telegram Bot API 8.0+ (июль 2026)
const BOT_TOKEN = process.env.BOT_TOKEN!;
const API = `https://api.telegram.org/bot${BOT_TOKEN}`;

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 sendOneTimeInvoice(chatId: number, orderId: string) {
  return call("sendInvoice", {
    chat_id: chatId,
    title: "Экспорт отчёта",
    description: "PDF-отчёт за текущий месяц",
    payload: `export:${orderId}`, // 1–128 байт, не показывается пользователю
    provider_token: "",
    currency: "XTR",
    prices: [{ label: "Экспорт", amount: 25 }],
    // start_parameter: если задан — влияет на поведение пересланных копий
    // (multi-chat vs deep-link на бота). См. docs.telegram.org/bots/payments-stars
  });
}

/** Переиспользуемая ссылка (чат, канал, Mini App, кнопка) */
export async function createShareableInvoiceLink(sku: string, stars: number) {
  return call<string>("createInvoiceLink", {
    title: "Цифровой товар",
    description: `SKU ${sku}`,
    payload: `sku:${sku}`,
    provider_token: "",
    currency: "XTR",
    prices: [{ label: sku, amount: stars }],
  });
}

type PreCheckoutQuery = {
  id: string;
  from: { id: number };
  currency: string;
  total_amount: number;
  invoice_payload: string;
};

export async function onPreCheckout(q: PreCheckoutQuery, stockLeft: number) {
  if (q.currency !== "XTR") {
    await call("answerPreCheckoutQuery", {
      pre_checkout_query_id: q.id,
      ok: false,
      error_message: "Поддерживаются только Stars (XTR)",
    });
    return;
  }

  if (stockLeft <= 0) {
    await call("answerPreCheckoutQuery", {
      pre_checkout_query_id: q.id,
      ok: false,
      error_message: "Товар закончился. Выбери другой.",
    });
    return;
  }

  // Проверь payload и сумму по своей БД заказов
  await call("answerPreCheckoutQuery", {
    pre_checkout_query_id: q.id,
    ok: true,
  });
}

type SuccessfulPayment = {
  currency: string;
  total_amount: number;
  invoice_payload: string;
  telegram_payment_charge_id: string;
  provider_payment_charge_id: string;
  is_recurring?: true;
  subscription_expiration_date?: number;
};

export async function onSuccessfulPayment(
  userId: number,
  payment: SuccessfulPayment,
) {
  // Идемпотентность: charge_id уникален
  const inserted = await dbInsertPaymentIfNew({
    userId,
    chargeId: payment.telegram_payment_charge_id,
    payload: payment.invoice_payload,
    stars: payment.total_amount,
  });
  if (!inserted) return; // повторный delivery update

  await fulfillOrder(payment.invoice_payload, userId);
  await call("sendMessage", {
    chat_id: userId,
    text: "Оплата получена. Товар выдан.",
  });
}

declare function dbInsertPaymentIfNew(row: {
  userId: number;
  chargeId: string;
  payload: string;
  stars: number;
}): Promise<boolean>;
declare function fulfillOrder(payload: string, userId: number): Promise<void>;

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

  • sendInvoice — инвойс как сообщение в chat_id (private / group / channel).
  • createInvoiceLink — строка-ссылка; удобно для Mini App и шаринга.
  • Для Stars в prices должен быть ровно один LabeledPrice.
  • Параметры вроде need_name / need_shipping_address для Stars игнорируются: цифровые платежи без ПДн.
  • На multi-use инвойсах бот сам решает, принимать ли повторные оплаты — валидируй остаток/лимиты в pre_checkout_query.
  • provider_payment_charge_id для Stars тоже приходит; для refund используй telegram_payment_charge_id.

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

Не отвечаешь на pre-checkout за 10 секунд → Telegram отменяет платёж. Держи проверку быстрой (кэш стока, без тяжёлых HTTP).

Принимаешь любой total_amount → сверяй сумму и payload со своей записью заказа. Иначе multi-use ссылку можно «переплатить/недоплатить» логикой бота, если ты сам это разрешишь.

Доставляешь товар дважды при повторном webhook → ключ идемпотентности: telegram_payment_charge_id.


Что дальше

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