Skip to content
Last updated

Оплата QR СБП

Пошаговое руководство по интеграции оплаты товаров и услуг через QR-коды СБП.

Сценарий

Ваш пользователь хочет оплатить покупку в магазине криптовалютой:

  1. Сканирует QR-код СБП на кассе
  2. Ваше приложение показывает сумму в USDT
  3. Пользователь подтверждает оплату
  4. Магазин получает рубли через СБП

Шаг 1: Получение QR-кода

Пользователь сканирует QR-код камерой. Вы получаете URL вида:

https://qr.nspk.ru/AS10003P3RCT8NTGH6LR6D3P815L3SHK?type=02&bank=100000000001&sum=50000&cur=RUB&crc=F4AB

Шаг 2: Создание котировки

Отправьте QR-код в API:

const response = await fetch('https://b2b.lumowallet.io/orders/prepare', {
  method: 'POST',
  headers: {
    'X-API-Key': process.env.LUMO_API_KEY,
    'Content-Type': 'application/json'
  },
  body: JSON.stringify({
    qrCode: qrCodeUrl,
    externalOrderId: `order-${Date.now()}`
  })
});

const quote = await response.json();

Ответ:

{
  "id": "550e8400-e29b-41d4-a716-446655440000",
  "amountRub": 5000.00,
  "amountUsdt": 52.63,
  "rate": 95.00,
  "expiresAt": "2026-03-10T12:00:30Z",
  "isBalanceSufficient": true,
  "merchantName": "ООО Кофейня"
}

Шаг 3: Показ пользователю

Отобразите информацию о платеже:

<PaymentConfirmation>
  <MerchantName>{quote.merchantName}</MerchantName>
  <Amount>{quote.amountRub} ₽</Amount>
  <CryptoAmount>{quote.amountUsdt} USDT</CryptoAmount>
  <Rate>Курс: {quote.rate} RUB/USDT</Rate>
  <Timer expiresAt={quote.expiresAt} />
  <ConfirmButton onClick={confirmPayment}>
    Оплатить
  </ConfirmButton>
</PaymentConfirmation>

⚠️ Важно: Котировка действует 30 секунд. Показывайте таймер!

Шаг 4: Подтверждение оплаты

При нажатии "Оплатить":

async function confirmPayment(quoteId) {
  const response = await fetch(
    `https://b2b.lumowallet.io/orders/accept/${quoteId}`,
    {
      method: 'POST',
      headers: {
        'X-API-Key': process.env.LUMO_API_KEY,
        'Idempotency-Key': `confirm-${quoteId}`
      }
    }
  );

  const order = await response.json();
  return order;
}

Шаг 5: Ожидание результата

После подтверждения ордер в статусе in_progress. Дождитесь webhook или polling:

Вариант A: Webhook (рекомендуется)

app.post('/webhooks/lumo', (req, res) => {
  const { event, orderId, status } = req.body;
  
  if (event === 'order.status_changed') {
    if (status === 'success') {
      notifyUser(orderId, 'Платёж успешен!');
    } else if (status === 'failed') {
      notifyUser(orderId, 'Платёж не удался');
    }
  }
  
  res.status(200).send('OK');
});

Вариант B: Polling

async function waitForCompletion(orderId, maxAttempts = 30) {
  for (let i = 0; i < maxAttempts; i++) {
    const response = await fetch(
      `https://b2b.lumowallet.io/orders/${orderId}`,
      { headers: { 'X-API-Key': process.env.LUMO_API_KEY } }
    );
    
    const order = await response.json();
    
    if (['success', 'failed', 'expired_no_taker'].includes(order.status)) {
      return order;
    }
    
    await new Promise(r => setTimeout(r, 2000));
  }
  
  throw new Error('Timeout waiting for order completion');
}

Шаг 6: Обработка результата

function handleOrderResult(order) {
  switch (order.status) {
    case 'success':
      showSuccess({
        message: 'Оплата прошла успешно!',
        merchant: order.merchantName,
        amount: order.amountRub
      });
      break;
      
    case 'failed':
      showError({
        message: 'Платёж не удался',
        reason: order.failureReason,
        action: 'Попробуйте снова'
      });
      break;
      
    case 'expired_no_taker':
      showError({
        message: 'Время ожидания истекло',
        action: 'Создайте новый платёж'
      });
      break;
  }
}

Полный пример

class LumoPaymentService {
  constructor(apiKey) {
    this.apiKey = apiKey;
    this.baseUrl = 'https://b2b.lumowallet.io';
  }

  async createQuote(qrCode) {
    const response = await fetch(`${this.baseUrl}/orders/prepare`, {
      method: 'POST',
      headers: {
        'X-API-Key': this.apiKey,
        'Content-Type': 'application/json'
      },
      body: JSON.stringify({ qrCode })
    });
    
    if (!response.ok) {
      throw new Error(`Quote failed: ${response.status}`);
    }
    
    return response.json();
  }

  async confirmQuote(quoteId) {
    const response = await fetch(`${this.baseUrl}/orders/accept/${quoteId}`, {
      method: 'POST',
      headers: {
        'X-API-Key': this.apiKey,
        'Idempotency-Key': `confirm-${quoteId}-${Date.now()}`
      }
    });
    
    if (!response.ok) {
      throw new Error(`Accept failed: ${response.status}`);
    }
    
    return response.json();
  }

  async getOrder(orderId) {
    const response = await fetch(`${this.baseUrl}/orders/${orderId}`, {
      headers: { 'X-API-Key': this.apiKey }
    });
    
    return response.json();
  }
}

// Использование
const lumo = new LumoPaymentService(process.env.LUMO_API_KEY);

async function processPayment(qrCode) {
  // 1. Создаём котировку
  const quote = await lumo.createQuote(qrCode);
  
  // 2. Проверяем баланс
  if (!quote.isBalanceSufficient) {
    throw new Error('Недостаточно средств');
  }
  
  // 3. Показываем пользователю (ждём подтверждения)
  const confirmed = await showConfirmationUI(quote);
  
  if (!confirmed) {
    return { cancelled: true };
  }
  
  // 4. Подтверждаем
  const order = await lumo.confirmQuote(quote.id);
  
  // 5. Ждём результат
  return await waitForCompletion(order.id);
}

Тестирование

Используйте staging-окружение:

const lumo = new LumoPaymentService(process.env.LUMO_API_KEY);
lumo.baseUrl = 'https://b2b-staging.lumowallet.io';

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

  • Получение и парсинг QR-кода
  • Создание котировки с обработкой ошибок
  • Таймер 30 секунд для котировки
  • Проверка isBalanceSufficient
  • Idempotency-Key при подтверждении
  • Обработка webhook'ов
  • Fallback на polling
  • Обработка всех статусов ордера
  • UI для успеха/ошибки