# Продажи через Telegram-бота

> Бот → ваш сервер → счёт → пуш или ссылка покупателю → вебхук → выдача товара. Защита от вала неоплаченных счетов, несколько брендов в одном боте, примеры на Python и Node.

## Коротко

Telegram-бот не обращается в Qut Pay напрямую — между ними стоит ваш сервер. Покупатель выбирает товар в боте → бот сообщает вашему серверу → сервер создаёт счёт → бот присылает картинку QR или ссылку на оплату (либо покупателю прилетает пуш в Kaspi) → после подтверждения к вам приходит вебхук → сервер говорит боту выдать товар. API-ключ живёт только на сервере, в коде бота его нет.

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

| Шаг | Кто | Что делает |
|---|---|---|
| 1 | Покупатель | Выбирает товар в боте и нажимает «Оплатить» |
| 2 | Бот | Шлёт запрос на ваш сервер (chat_id, товар, сумма) |
| 3 | Сервер | `POST /api/v1/invoices`, кладёт `chat_id` в `metadata` |
| 4 | Бот | Отправляет картинку `qrImageUrl` или ссылку `payUrl` |
| 5 | Покупатель | Подтверждает оплату в Kaspi |
| 6 | Qut Pay | Шлёт на сервер `invoice.paid` |
| 7 | Сервер | По `metadata.chat_id` сообщает боту, бот выдаёт товар |

Положить `chat_id` в `metadata` — ключевая деталь схемы: когда придёт вебхук, вы сразу знаете, кому отвечать, и не ищете покупателя по своей базе.

## Какой метод API используется

Создание счёта — `POST /api/v1/invoices`. Два вида:

- `kind: "qr"` — возвращает QR и ссылку. Покупатель видит картинку в боте или жмёт ссылку. Описание — до 100 символов.
- `kind: "phone"` — покупателю приходит пуш в приложение Kaspi, обязателен `customer.phone` в формате `7XXXXXXXXXX`. Сумма — целые тенге, описание — 60 символов.

Если номер покупателя известен, в боте удобнее `phone`: ничего сканировать не нужно, Kaspi открывается сам. Номер можно получить кнопкой «Отправить контакт» прямо в Telegram.

Остальное: `GET /api/v1/invoices/{id}` — статус, `POST /api/v1/invoices/{id}/cancel` — отмена, `POST /api/v1/invoices/{id}/refund` — возврат.

## Короткий пример

Python:

```python
import requests

r = requests.post(
    "https://api.qut.kz/api/v1/invoices",
    headers={"X-API-Key": API_KEY, "Idempotency-Key": f"tg-{chat_id}-{cart_id}"},
    json={
        "amount": 5900,
        "kind": "qr",
        "description": "Курс: первый модуль",
        "externalOrderId": str(cart_id),
        "metadata": {"chat_id": chat_id, "bot": "kurs_bot"},
    },
    timeout=15,
)
inv = r.json()
bot.send_photo(chat_id, inv["qrImageUrl"], caption=f"Оплатить: {inv['payUrl']}")
```

Node.js:

```js
const res = await fetch('https://api.qut.kz/api/v1/invoices', {
  method: 'POST',
  headers: {
    'X-API-Key': process.env.QUTPAY_KEY,
    'Idempotency-Key': `tg-${chatId}-${cartId}`,
    'Content-Type': 'application/json',
  },
  body: JSON.stringify({
    amount: 5900,
    kind: 'phone',
    description: 'Курс: первый модуль',
    customer: { phone: '77010000000' },
    metadata: { chat_id: chatId, bot: 'kurs_bot' },
  }),
});
const inv = await res.json();
```

На стороне вебхука берёте сырое тело, считаете `HMAC-SHA256(secret, timestamp + "." + rawBody)`, сравниваете с заголовком и по `payload.metadata.chat_id` сообщаете боту. Есть готовые SDK: Node.js, PHP, Python.

## Как не накопить неоплаченные счета

В боте это реальная проблема: люди жмут «Оплатить» и уходят. Несколько сотен открытых счетов могут упереться в суточную защиту.

- **Один покупатель — один открытый счёт.** Перед созданием нового отмените прежний через `cancel` или проверьте его статус. Храните у себя связку `chat_id → последний invoice id`.
- **Ставьте `Idempotency-Key`.** Даже если кнопку нажали пять раз, счёт будет один. Включите в ключ `chat_id` и номер корзины.
- **Блокируйте кнопку.** После создания счёта уберите «Оплатить» или замените на «Ожидаем оплату».
- **Убирайте истёкшие QR сами.** Когда прошло время `expiresAt`, отредактируйте сообщение и поставьте кнопку «Получить новый счёт».
- **Помните: суточное число — не бизнес-лимит**, а защита от интеграции, ушедшей в цикл. Она даёт ошибку `tariff_daily_burst`, а месячный лимит — `tariff_limit_reached`. Это разные вещи: [Какой тариф выбрать](/kb/ru/tariff-choose).

## Несколько брендов в одном боте

Если в боте продаётся несколько магазинов или направлений, есть два пути.

**Одна организация, разная пометка.** Все деньги приходят на один счёт в Kaspi. Бренд пишете в `metadata` (`{"brand": "shop_a"}`) и по нему разделяете отчётность. Самый простой вариант.

**Разные ключи.** Для каждого направления заводите отдельный API-ключ — тогда счета удобно фильтровать по источнику. Ключ можно привязать к конкретному кассиру: привязанный ключ видит только счета этого кассира, на чужие отвечает 404. Про кассиров: [Можно ли подключить несколько кассиров](/kb/ru/two-cashiers).

**Если деньги должны идти на разные счета** — это уже разные организации, у каждой свой кассир и свой тариф: [Несколько организаций в одном аккаунте](/kb/ru/multiple-organizations).

## На что обратить внимание

- **Не путайте токен бота и API-ключ.** Оба лежат на сервере, но API-ключ никогда не передаётся в Telegram.
- **Поздняя оплата.** Если деньги придут на истёкший счёт, событие `invoice.paid` придёт с признаком `late: true`. Обработайте это в боте: либо выдайте товар, либо верните деньги.
- **Обработчик вебхука должен быть идемпотентным.** Одно событие может прийти повторно; обрабатывайте пару `(invoice.id, status)` один раз, иначе покупатель получит товар дважды.
- **У нас есть свой бот.** Он не для продаж, а для контроля: `/invoice`, `/today`, `/last`, `/status`, `/cancel`, `/support`. Подключите его рядом, чтобы видеть счета с телефона: [руководство по Telegram-боту](https://api.qut.kz/docs/guide/telegram_bot).
- **Прогоните весь цикл в песочнице**: счёт → `simulate` → вебхук → бот выдал товар.

## Вопросы и ответы

**Можно положить API-ключ в код бота?** Нет. Даже если бот и ключ на одном сервере, читайте ключ из переменной окружения и не коммитьте в репозиторий. Если ключ утёк — удалите его и создайте новый.

**А если я не знаю номер покупателя?** Делайте QR-счёт, для него номер не нужен. Пуш на телефон работает только при `kind: "phone"`.

**Если бот упал, деньги потеряются?** Нет. Оплата проходит в Kaspi, деньги приходят на ваш счёт. Когда бот поднимется, вебхук придёт снова — мы повторяем доставку 11 раз, пока не получим 2xx.

**Можно продавать в групповом чате?** Бота можно добавить в группу, но ссылку на оплату лучше слать в личный чат: в ней видны сумма и описание.

**Покупатель может отсканировать QR прямо из бота?** На одном телефоне это неудобно. Поэтому в боте лучше давать ссылку `payUrl` или пуш `kind: "phone"`, а картинку QR оставить запасным вариантом.
