Qut Pay Сайт Кабинет Білім базасы Нұсқаулықтар API құжаттамасы ҚАЗРУС
Басты бет → Білім базасы → Анықтамалық

Kaspi Pay-ді Next.js-ке қосу

Жаңартылды: 2026-10-25 · Markdown нұсқасы

Қысқаша

Next.js-те (app router) Qut Pay арқылы Kaspi QR интеграциясы — екі Route Handler. POST /api/pay серверде X-API-Key тақырыбымен POST https://api.qut.kz/api/v1/invoices шақырып, сатып алушыны жіберетін payUrl қайтарады. POST /api/qutpay/webhook денені await req.text() арқылы сол күйінде оқиды, X-Webhook-Signature-ді (sha256= + timestamp + "." + rawBody-дің HMAC-SHA256-сы) crypto.timingSafeEqual арқылы тексереді, тапсырысты бір рет төленді деп белгілеп, 200 қайтарады. Кілт пен құпия — тек серверлік QUTPAY_API_KEY және QUTPAY_WEBHOOK_SECRET айнымалыларында, NEXT_PUBLIC_ префиксінсіз.

Дайындық

  1. https://qut.kz/app кабинетінде API кілтін жасаңыз (sandbox үшін qp_test_…) және Интеграциялар бөлімінде https://site.kz/api/qutpay/webhook webhook адресін қосыңыз; құпия бір рет көрсетіледі — webhook баптау.
  2. .env.local ішінде: QUTPAY_API_KEY=qp_test_… және QUTPAY_WEBHOOK_SECRET=…. NEXT_PUBLIC_ префиксі бар кез келген айнымалы браузер бандліне түседі — кілт үшін бұл ағып кету.
  3. Node.js SDK (https://api.qut.kz/downloads/qutpay-sdk-node.zip) міндетті емес: төмендегі мысал кірістірілген fetch және node:crypto арқылы. SDK қоссаңыз, verifyWebhook({ secret, rawBody, headers: req.headers }) Route Handler-дің Headers объектісін тікелей қабылдайды — Node.js SDK.
  4. Екі өңдеуші де Node.js runtime-да жұмыс істеуі керек (әдепкі мән); node:crypto себебінен webhook үшін Edge runtime жарамайды.

Код: төлем және webhook

// app/api/pay/route.ts
import { NextResponse } from 'next/server';

export async function POST(req: Request) {
  const { orderId } = await req.json();
  const order = await getOrder(orderId);                        // соманы сұраудан емес, базадан аламыз
  const r = await fetch('https://api.qut.kz/api/v1/invoices', {
    method: 'POST',
    headers: { 'X-API-Key': process.env.QUTPAY_API_KEY!, 'Content-Type': 'application/json',
               'Idempotency-Key': `order-${order.id}` },
    body: JSON.stringify({ amount: order.total, externalOrderId: String(order.id),
      description: `Тапсырыс №${order.id}`, successUrl: `https://site.kz/thanks?order=${order.id}`,
      failUrl: `https://site.kz/orders/${order.id}`, metadata: { orderId: order.id } }),
  });
  if (!r.ok) return NextResponse.json({ error: (await r.json()).error }, { status: 502 });
  const inv = await r.json();                                   // id, status, payUrl, qrUrl, deepLink, expiresAt
  await saveInvoiceId(order.id, inv.id);
  return NextResponse.json({ payUrl: inv.payUrl });             // клиентте: window.location.href = payUrl
}

// app/api/qutpay/webhook/route.ts
import crypto from 'node:crypto';

export async function POST(req: Request) {
  const raw = await req.text();                                 // шикі дене — JSON.parse-қа дейін
  const ts = req.headers.get('x-webhook-timestamp') ?? '';
  const sig = req.headers.get('x-webhook-signature') ?? '';
  const expected = 'sha256=' + crypto.createHmac('sha256', process.env.QUTPAY_WEBHOOK_SECRET!)
    .update(`${ts}.${raw}`).digest('hex');
  const fresh = Math.abs(Date.now() / 1000 - Number(ts)) <= 300;
  const a = Buffer.from(sig), b = Buffer.from(expected);
  if (!fresh || a.length !== b.length || !crypto.timingSafeEqual(a, b)) return new Response(null, { status: 401 });

  const { event, invoice } = JSON.parse(raw);
  if (event === 'invoice.paid') {
    // идемпотентті: UPDATE orders SET paid_at = now() WHERE id = $1 AND paid_at IS NULL AND total = $2
    await markPaidOnce(invoice.externalOrderId, invoice.amount, invoice.id);
  }
  return new Response(null, { status: 200 });                   // 8 секунд ішінде, әйтпесе қайталанады
}

«Рахмет» беті

Сатып алушы successUrl-ге оралады, бірақ бұл өтуді қолмен де ашуға болады — ол ештеңе дәлелдемейді. app/thanks/page.tsx (серверлік компонент) ішінде тапсырысты базадан оқып, «төленді» деп тек paid_at толтырылғанда көрсетіңіз; webhook әлі жолда болса, күйді тікелей сұраңыз: X-API-Key-мен GET https://api.qut.kz/api/v1/invoices/{id}?live=1 — серверде, браузерден емес. Толығырақ: төлемді автоматты қалай білуге болады.

Неге назар аудару керек

ЖағдайНеге маңызды
req.json() емес, await req.text()HMAC байттар бойынша есептеледі; талданып, қайта жиналған JSON ешқашан сәйкес келмейді — қолтаңба сәйкес келмейді
timingSafeEqual және ұзындық тексеруі=== арқылы салыстыру уақыт бойынша шабуылға осал; ұзындық әртүрлі болса timingSafeEqual ерекшелік тастайды
Идемпотенттілік2xx болмаса, жеткізу ~2 сағат ішінде 11 ретке дейін қайталанады; paid_at IS NULL шарты қайталауды зиянсыз етеді — идемпотенттілік
Middlewaremiddleware.ts авторизация талап етсе, /api/qutpay/webhook-ты ерекшелікке қосыңыз — біз логинсіз келеміз

Sandbox-та тексеру

  1. qp_test_… кілтімен POST /api/pay шақырып, payUrl-ді ашыңыз.
  2. Төлемді имитациялаңыз: {"status":"paid"} денесімен POST https://api.qut.kz/api/v1/invoices/{id}/simulate — webhook қолтаңбасымен нағыз күйінде кетеді — sandbox. Локалда webhook адресі http және туннель бола алады; нақты режимде — тек нағыз домендегі https.
  3. Тапсырыс paid_at алғанына, ал қайталанған жеткізу ештеңе өзгертпейтініне көз жеткізіңіз; содан соң qp_live_… кілтіне ауысыңыз — іске қосу алдындағы тексеру тізімі.

Сұрақ-жауап

Pages router жарай ма? Иә, бірақ онда парсерді өшіру керек: export const config = { api: { bodyParser: false } } және денені ағыннан қолмен жинау; app router-де req.text() шикі денені баптаусыз береді.

Qut Pay API-ін клиенттік компоненттен шақыруға бола ма? Жоқ: кілт браузерге түседі. Тек серверде — Route Handler немесе Server Action.

Күту бетінде polling керек пе? 5–10 секунд сайын өз GET /api/orders/{id}-іңізден сұрауға болады; ақиқат көзі — webhook — webhook пе, сұрау ма.

Байланысты мақалалар

Node.js SDKQut Pay-дің Node.js клиентін орнату, счёт жасау, күйін сұрау, қайтару және webhook қолтаңбасын тексеру. Толық жұмыс істейтін Express мысалы raw body-мен және қате өңдеу.Webhook қауіпсіздігі және қолтаңбаны тексеруҚолтаңба қалай құралады, неге raw body міндетті, timestamp-ты қалай тексеру керек, Express, Laravel, Django және таза Node үшін код мысалдары, идемпотентті өңдеу және жиі кездесетін қателер.Kaspi QR төленгенін автоматты қалай білуге боладыKaspi QR төлемін қолмен салыстырмай растаудың екі тәсілі — серверіңізге invoice.paid webhook-і және счёт күйін сұрау. Не келеді, қолтаңбаны қалай тексеру, тапсырысты қателесіп төленді деп белгілемеу, «рахмет» бетінде не істеу.Sandbox-та төлемді симуляциялауsimulate эндпоинті арқылы sandbox счётының күйін өзгерту: paid, failed, expired. Kaspi шақырылмайды, webhook нағыз төлемдегідей келеді. Сынау циклі және автоматты тест жазу.Идемпоттылық: қайталаудан қорғануIdempotency-Key тақырыбы қалай жұмыс істейді, кілтті қалай құру керек, externalOrderId-дің рөлі неде, webhook өңдеуде және қайтаруда қайталанудан қалай сақтану керек.Webhook қолтаңбасы сәйкес келмейдіҚолтаңба HMAC-SHA256(secret, timestamp + "." + rawBody) арқылы есептеледі. Ең жиі қате — денені JSON-ға айналдырып, қайта жолға түрлендіру. Raw body алудың Express, Laravel, Django мысалдары.

Сұрағыңыз қалды ма? WhatsApp +77788813333 · kazprose@gmail.com
Кабинеттен де жазуға болады: Қолдау.

Qut Pay — тәуелсіз сервис, «Kaspi Bank» АҚ-мен аффилирленбеген. Kaspi және Kaspi Pay — құқық иесінің тауар белгілері.