iFragment

Пример интеграции: Telegram-бот магазина

Как бот вроде StarPay заменяет свой код Fragment/TON вызовами iFragment API.

Типичный бот-магазин Stars (как StarPay) сам ходит во Fragment: держит cookies сессии, мнемонику TON-кошелька, подписывает переводы, следит за seqno и повторами. С iFragment вся эта часть заменяется тремя вызовами API и одним вебхуком. Оплата покупателя, каталог и интерфейс бота остаются вашими.

Было и стало

Шаг ботаБыло (свой код)Стало (iFragment)
Проверить usernamesearchStarsRecipient во Fragment с cookiesGET /v1/recipients/lookup
Купить после оплатыinitBuyStarsRequest → getBuyStarsLink → подпись и отправка TONPOST /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:

ifragment.ts
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:

on-paid.ts
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 вашего заказа, по нему находим покупателя:

webhook.ts
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');
}

Чек-лист миграции

  1. Получите ключ Project и секрет вебхука; включите IP allowlist на адрес бота.
  2. Если остаётесь со своими аккаунтами, передайте сессию и кошелёк через кабинет (режим OWN). Кошелёк после этого используйте только через iFragment.
  3. Замените поиск получателя на GET /v1/recipients/lookup.
  4. Замените покупку на POST /v1/orders с externalId = id заказа бота.
  5. Добавьте endpoint вебхука, проверку подписи и дедупликацию по event.id.
  6. Удалите код Fragment/TON: cookies, мнемонику, подпись переводов, опрос seqno.

На этой странице