# Идемпоттылық: қайталаудан қорғану

> Idempotency-Key тақырыбы қалай жұмыс істейді, кілтті қалай құру керек, externalOrderId-дің рөлі неде, webhook өңдеуде және қайтаруда қайталанудан қалай сақтану керек.

## Қысқаша

Идемпоттылық — бір әрекетті екі рет орындасаңыз, нәтиже бір рет орындағандағыдай болуы. Төлемде бұл екі жерде керек:

1. **Счёт жасағанда** — `Idempotency-Key` тақырыбы. Сол кілтпен қайталасаңыз жаңа счёт жасалмайды, бұрынғысы қайтады: HTTP 200 және жауапта `idempotentReplay: true`.
2. **Webhook өңдегенде** — `(invoice.id, status)` жұбы бойынша бір рет орындау.

Үшінші жер — **қайтару**, онда логика бөлек: белгісіз нәтижені соқыр қайталауға болмайды.

## Idempotency-Key қалай жұмыс істейді

```bash
curl -X POST https://api.qut.kz/api/v1/invoices \
  -H 'X-API-Key: qp_live_…' \
  -H 'Content-Type: application/json' \
  -H 'Idempotency-Key: order-1001-pay' \
  -d '{ "amount": 2500, "externalOrderId": "1001" }'
```

| Сұрау | Нәтиже |
|---|---|
| Бірінші рет | HTTP 201, жаңа счёт жасалды |
| Сол кілтпен қайталау | HTTP 200, сол счёт қайтады, `idempotentReplay: true` |
| Басқа кілтпен | HTTP 201, жаңа счёт |

Кілт жібермесеңіз, әр сұрау жаңа счёт жасайды — сондықтан клиент «төлеу» батырмасын екі рет бассаңыз, екі счёт шығады.

Node SDK-да бұл `idempotencyKey` параметрі арқылы беріледі:

```js
const inv = await qp.createInvoice({
  amount: 2500,
  description: 'Тапсырыс №1001',
  externalOrderId: '1001',
  idempotencyKey: `order-${order.id}`,
});
```

## Кілтті қалай құру керек

Кілт — **бір нақты әрекетті** сипаттайтын тұрақты жол. Дұрыс құрылым: тапсырыс нөмірі + әрекет.

| Не | Мысал | Неге |
|---|---|---|
| ✅ Тапсырыс + әрекет | `order-1001-pay` | Бір тапсырысқа бір счёт |
| ✅ Тапсырыс + әрекет + талпыныс | `order-1001-pay-2` | Клиент әдейі жаңа счёт сұрағанда |
| ✅ Жазылым + кезең | `sub-88-2026-09` | Ай сайын бір счёт |
| ❌ Кездейсоқ UUID | `a3f1…` | Әр сұрауда жаңа, қорғамайды |
| ❌ Уақыт белгісі | `1757167000` | Әр сұрауда жаңа, қорғамайды |
| ❌ Тек тапсырыс нөмірі | `1001` | Бір тапсырысқа екінші рет счёт керек болса, тығырық |

Басты ереже: **кілт сіздің жүйеңізде есептеліп шығуы керек**, яғни қайталап сұрау жібергенде дәл сол кілт қайта шығатын болсын. Егер кілтті әр сұрауда кездейсоқ генерациялап отырсаңыз, ол ешнәрседен қорғамайды.

Кілтті базада тапсырыспен бірге сақтап қойған дұрыс: сонда таймаут болып, сервер қайта іске қосылса да, сол кілтпен қайталай аласыз.

## Таймаут болғанда

Ең қауіпті сәт — сұрау кетті, ал жауап келмеді. Счёт жасалды ма, жоқ па — белгісіз.

`Idempotency-Key` болса, жауап қарапайым: **дәл сол кілтпен қайталаңыз**. Счёт жасалып қойған болса, бұрынғысы қайтады; жасалмаған болса, жаңасы жасалады. Екі жағдайда да бір ғана счёт болады.

```js
async function createInvoiceSafely(order) {
  const key = `order-${order.id}-pay`;
  for (let i = 0; i < 3; i++) {
    try {
      return await qp.createInvoice({ amount: order.total, externalOrderId: String(order.id), idempotencyKey: key });
    } catch (e) {
      if (i === 2) throw e;
      await new Promise((r) => setTimeout(r, 2 ** i * 1000));
    }
  }
}
```

## externalOrderId-дің рөлі

`externalOrderId` — сіздің тапсырыс нөміріңіз. Ол **қорғаныс құралы емес**: бір `externalOrderId` мәнімен қалағаныңызша счёт жасай аласыз, ешкім тоқтатпайды.

| | `Idempotency-Key` | `externalOrderId` |
|---|---|---|
| Не істейді | Қайталанған сұрауды тоқтатады | Счётты тапсырысыңызбен байланыстырады |
| Қайда беріледі | HTTP тақырыбында | Счёттың денесінде |
| Webhook-та келе ме | Жоқ | Иә |
| Іздеуге жарай ма | Жоқ | Иә |
| Қайталаудан қорғай ма | **Иә** | Жоқ |

Екеуін бірге қолданыңыз: `Idempotency-Key` қосарлануды болдырмайды, `externalOrderId` webhook келгенде қай тапсырыс екенін бірден табуға көмектеседі. Толығырақ: [Metadata және тапсырыс нөмірі](/kb/metadata-and-orders).

## Webhook өңдеуде идемпоттылық

Біз 2xx емес жауапта сұрауды 11 рет қайталаймыз. Желі үзілсе, сіз 200 қайтаруға үлгермесеңіз, өңдеу екінші рет келеді. Сондықтан өңдеуді `(invoice.id, status)` жұбы бойынша **бір рет** орындаңыз.

```sql
CREATE TABLE qutpay_events (
  invoice_id TEXT NOT NULL,
  status     TEXT NOT NULL,
  handled_at TIMESTAMPTZ DEFAULT now(),
  PRIMARY KEY (invoice_id, status)
);
```

```js
const ins = await db.query(
  'INSERT INTO qutpay_events (invoice_id, status) VALUES ($1, $2) ON CONFLICT DO NOTHING',
  [invoice.id, invoice.status],
);
if (ins.rowCount === 0) return res.sendStatus(200);  // бұрын өңделген
await fulfil(invoice);
```

Неге `invoice.id` жалғыз жеткіліксіз: бір счёт бойынша бірнеше түрлі күй келеді (`pending`, `paid`, сосын `refunded`). Әрқайсысын бөлек өңдеу керек, бірақ әрқайсысын бір рет қана.

Webhook-пен қатар күйді сұрап отырсаңыз (вендинг, турникет сияқты кідіріске сезімтал сценарийлер), идемпоттылық бұдан да маңызды: екі арна бір нәтижені екі рет әкелуі мүмкін, ал құрылғы екі рет ашылмауы тиіс. Қолтаңбаны тексерумен бірге: [Webhook қауіпсіздігі](/kb/webhook-security).

## Қайтаруда идемпоттылық

Қайтару — ақша қозғалысы, сондықтан оны соқыр қайталауға **болмайды**. Екі қате коды бар, екеуі де «нәтижесі белгісіз» дегенді білдіреді:

| Код | HTTP | Мағынасы |
|---|---|---|
| `refund_unknown` | 502 | Kaspi жауап бермеді, қайтару өтті ме, өтпеді ме — белгісіз |
| `refund_pending_unknown` | 409 | Алдыңғы қайтарудың нәтижесі әлі белгісіз |

Осы екеуін көрсеңіз реті мынандай:

1. **Қайтаруды қайталамаңыз.**
2. `GET /api/v1/invoices/{id}` арқылы счёттың күйін оқыңыз — жауапта қайтарулар тізімі де келеді.
3. Күйі `refunded` немесе `partially_refunded` болса, қайтару өткен. Ештеңе істемеңіз.
4. Күйі әлі `paid` болса, біраз күтіп қайта оқыңыз.
5. Жағдай ұзақ анықталмаса, қолдауға жазыңыз: WhatsApp +7 778 881 3333, Telegram @qutpaybot.

```js
try {
  await qp.refund(invoiceId, { amount });
} catch (e) {
  if (e.error === 'refund_unknown' || e.error === 'refund_pending_unknown') {
    await waitAndCheckState(invoiceId);   // қайталамау
  } else {
    throw e;
  }
}
```

Толығырақ: [Қайтару API](/kb/refunds-api) және [Екі рет қайтарып жіберуден қалай сақтану](/kb/double-refund).

## Счёттар қосарланып кетсе

Егер қосарланған счёттар шығып жатса, бұл әдетте кодтағы цикл немесе идемпоттылықтың жоқтығы. Шұғыл қадамдар: [Счёттар қосарланып жатыр](/kb/duplicate-invoices).

Қосарлану тарифтің айлық лимитін де тез жеп қояды: лимит жасалған счёт бойынша есептеледі, төленгені бойынша емес.

## Жиі қойылатын сұрақтар

**Кілттің жарамдылық мерзімі бар ма?** Кілт шексіз сақталмайды. Бір тапсырысты бірнеше ай өткен соң қайта жіберсеңіз, жаңа счёт жасалуы мүмкін — сондықтан тапсырыстың күйін өз базаңыздан да тексеріңіз.

**Сол кілтпен басқа сомамен жіберсем ше?** Кілт бұрынғы счётты қайтарады. Сома шынымен өзгерсе, жаңа кілт қолданыңыз: мысалы `order-1001-pay-2`.

**Sandbox-та да жұмыс істей ме?** Иә, бірдей.

**Топтап счёт жасағанда ше?** `POST /api/v1/invoices/bulk` ішіндегі әр элемент жеке тексеріледі. Тізімге әр тапсырыс бір рет кіретініне өз жағыңызда көз жеткізіңіз: [Топтап счёт жасау](/kb/bulk-invoices).

**Жазылымда идемпоттылық керек пе?** Жазылым счёттарды өзі кестемен шығарады, ол жақ біздің жағымызда реттелген. Сізге тек webhook өңдеуін идемпотентті жазу керек.
