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

> POST /api/pay Route Handler-і Qut Pay счётын серверде жасайды (кілт браузерге түспейді) және payUrl қайтарады; POST /api/qutpay/webhook шикі денені req.text() арқылы оқып, HMAC-ты timingSafeEqual-мен тексереді; «рахмет» беті күйді серверде қайта тексереді. QUTPAY_API_KEY және QUTPAY_WEBHOOK_SECRET айнымалылары.

## Қысқаша

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 баптау](/kb/webhook-setup).
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](/kb/sdk-node).
4. Екі өңдеуші де Node.js runtime-да жұмыс істеуі керек (әдепкі мән); `node:crypto` себебінен webhook үшін Edge runtime жарамайды.

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

```ts
// 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` — серверде, браузерден емес. Толығырақ: [төлемді автоматты қалай білуге болады](/kb/auto-check-payment).

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

| Жағдай | Неге маңызды |
|---|---|
| `req.json()` емес, `await req.text()` | HMAC байттар бойынша есептеледі; талданып, қайта жиналған JSON ешқашан сәйкес келмейді — [қолтаңба сәйкес келмейді](/kb/webhook-signature-mismatch) |
| `timingSafeEqual` және ұзындық тексеруі | `===` арқылы салыстыру уақыт бойынша шабуылға осал; ұзындық әртүрлі болса `timingSafeEqual` ерекшелік тастайды |
| Идемпотенттілік | `2xx` болмаса, жеткізу ~2 сағат ішінде 11 ретке дейін қайталанады; `paid_at IS NULL` шарты қайталауды зиянсыз етеді — [идемпотенттілік](/kb/idempotency) |
| Middleware | `middleware.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](/kb/sandbox-simulate). Локалда webhook адресі `http` және туннель бола алады; нақты режимде — тек нағыз домендегі `https`.
3. Тапсырыс `paid_at` алғанына, ал қайталанған жеткізу ештеңе өзгертпейтініне көз жеткізіңіз; содан соң `qp_live_…` кілтіне ауысыңыз — [іске қосу алдындағы тексеру тізімі](/kb/going-live-checklist).

## Сұрақ-жауап

**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 пе, сұрау ма](/kb/polling-vs-webhook).
