Пример интеграции: Telegram-бот магазина
Как бот вроде StarPay заменяет свой код Fragment/TON вызовами iFragment API.
Типичный бот-магазин Stars (как StarPay) сам ходит во Fragment: держит cookies сессии, мнемонику TON-кошелька, подписывает переводы, следит за seqno и повторами. С iFragment вся эта часть заменяется тремя вызовами API и одним вебхуком. Оплата покупателя, каталог и интерфейс бота остаются вашими.
Было и стало
| Шаг бота | Было (свой код) | Стало (iFragment) |
|---|---|---|
| Проверить username | searchStarsRecipient во Fragment с cookies | GET /v1/recipients/lookup |
| Купить после оплаты | initBuyStarsRequest → getBuyStarsLink → подпись и отправка TON | POST /v1/orders с externalId = id вашего заказа |
| Узнать результат | опрос seqno, сверка транзакций, ручные разборы | вебхук order.succeeded / order.failed |
| Сессия, кошелёк, троттлинг, повторы | ваши | наши (Supplier Accounts) |
Если у вас уже есть своя сессия и кошелёк, подключите их как Supplier Account в режиме OWN: платить поставщику будете по-прежнему вы, а платформа возьмёт на себя выполнение.
Поток
Покупатель ─▶ бот: /buy 500 → @durov
бот ─▶ iFragment: GET /v1/recipients/lookup → имя и аватар, «Найден»
Покупатель ─▶ ваш платёж (Payme, Click, Stars, …) → оплачено
бот ─▶ iFragment: POST /v1/orders { externalId: "<id заказа бота>" } → 201 QUEUED
iFragment ─▶ бот: вебхук order.succeeded → «Готово! 500 ⭐ у @durov»Код (TypeScript, fetch)
Клиент iFragment занимает несколько строк поверх fetch:
const API = 'https://api.ifragment.com/v1';
async function call<T>(path: string, init: RequestInit = {}): Promise<T> {
const res = await fetch(`${API}${path}`, {
...init,
headers: {
'X-API-Key': process.env.IFRAGMENT_KEY!,
'Content-Type': 'application/json',
'Accept-Language': 'ru',
...init.headers,
},
});
const body = await res.json();
if (!body.success) {
throw Object.assign(new Error(body.error.message), { status: res.status, code: body.error.code });
}
return body.data as T;
}
export const ifragment = {
lookup: (username: string) =>
call<{ found: boolean; name?: string }>(`/recipients/lookup?username=${encodeURIComponent(username)}&product=STARS`),
// externalId = id заказа в боте: повтор после таймаута не создаст второй заказ.
createStarsOrder: (shopOrderId: string, username: string, quantity: number) =>
call<{ id: string; status: string }>('/orders', {
method: 'POST',
headers: { 'Idempotency-Key': `shop-order-${shopOrderId}` },
body: JSON.stringify({ product: 'STARS', quantity, username, externalId: shopOrderId }),
}),
};Покупка в боте после успешной оплаты, вместо прежнего кода Fragment/TON:
import { ifragment } from './ifragment';
export async function onShopOrderPaid(order: { id: string; username: string; quantity: number; chatId: number }) {
try {
await ifragment.createStarsOrder(order.id, order.username, order.quantity);
await db.shopOrders.update(order.id, { status: 'FULFILLING' });
await bot.api.sendMessage(order.chatId, 'Оплата получена, отправляем звёзды…');
} catch (error) {
// 422: получатель пропал между проверкой и оплатой, вернуть деньги покупателю.
// 5xx/сеть: повторить позже тем же вызовом: externalId и Idempotency-Key защитят от дубля.
await retryQueue.add('create-order', order, { delay: 30_000 });
}
}Вебхук: проверка подписи и уведомление покупателя. data.externalId это id вашего заказа, по нему находим покупателя:
import { createHmac, timingSafeEqual } from 'node:crypto';
function verify(raw: string, header: string, secret: string): boolean {
const parts = Object.fromEntries(header.split(',').map((p) => p.split('=', 2)));
if (!parts.t || !parts.v1 || Math.abs(Date.now() / 1000 - Number(parts.t)) > 300) {
return false;
}
const expected = createHmac('sha256', secret).update(`${parts.t}.${raw}`).digest();
const received = Buffer.from(parts.v1, 'hex');
return received.length === expected.length && timingSafeEqual(expected, received);
}
export async function handleWebhook(request: Request): Promise<Response> {
const raw = await request.text(); // сырое тело, до JSON.parse
if (!verify(raw, request.headers.get('X-IFragment-Signature') ?? '', process.env.IFRAGMENT_WEBHOOK_SECRET!)) {
return new Response('bad signature', { status: 401 });
}
const event = JSON.parse(raw) as { id: string; type: string; data: { externalId: string; quantity: number } };
if (await db.processedEvents.exists(event.id)) {
return new Response('ok'); // повторная доставка
}
const order = await db.shopOrders.find(event.data.externalId);
if (event.type === 'order.succeeded') {
await db.shopOrders.update(order.id, { status: 'COMPLETED' });
await bot.api.sendMessage(order.chatId, `Готово! ${event.data.quantity} ⭐ уже у @${order.username}`);
} else if (event.type === 'order.failed') {
await db.shopOrders.update(order.id, { status: 'FAILED' });
await bot.api.sendMessage(order.chatId, 'Не удалось выполнить заказ, вернём деньги в течение дня.');
}
await db.processedEvents.add(event.id);
return new Response('ok');
}Чек-лист миграции
- Получите ключ Project и секрет вебхука; включите IP allowlist на адрес бота.
- Если остаётесь со своими аккаунтами, передайте сессию и кошелёк через кабинет (режим
OWN). Кошелёк после этого используйте только через iFragment. - Замените поиск получателя на
GET /v1/recipients/lookup. - Замените покупку на
POST /v1/ordersсexternalId= id заказа бота. - Добавьте endpoint вебхука, проверку подписи и дедупликацию по
event.id. - Удалите код Fragment/TON: cookies, мнемонику, подпись переводов, опрос seqno.