Қысқаша
Идемпоттылық — бір әрекетті екі рет орындасаңыз, нәтиже бір рет орындағандағыдай болуы. Төлемде бұл екі жерде керек:
- Счёт жасағанда —
Idempotency-Keyтақырыбы. Сол кілтпен қайталасаңыз жаңа счёт жасалмайды, бұрынғысы қайтады: HTTP 200 және жауаптаidempotentReplay: true. - Webhook өңдегенде —
(invoice.id, status)жұбы бойынша бір рет орындау.
Үшінші жер — қайтару, онда логика бөлек: белгісіз нәтижені соқыр қайталауға болмайды.
Idempotency-Key қалай жұмыс істейді
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 параметрі арқылы беріледі:
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 болса, жауап қарапайым: дәл сол кілтпен қайталаңыз. Счёт жасалып қойған болса, бұрынғысы қайтады; жасалмаған болса, жаңасы жасалады. Екі жағдайда да бір ғана счёт болады.
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 және тапсырыс нөмірі.
Webhook өңдеуде идемпоттылық
Біз 2xx емес жауапта сұрауды 11 рет қайталаймыз. Желі үзілсе, сіз 200 қайтаруға үлгермесеңіз, өңдеу екінші рет келеді. Сондықтан өңдеуді (invoice.id, status) жұбы бойынша бір рет орындаңыз.
CREATE TABLE qutpay_events (
invoice_id TEXT NOT NULL,
status TEXT NOT NULL,
handled_at TIMESTAMPTZ DEFAULT now(),
PRIMARY KEY (invoice_id, status)
);
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 қауіпсіздігі.
Қайтаруда идемпоттылық
Қайтару — ақша қозғалысы, сондықтан оны соқыр қайталауға болмайды. Екі қате коды бар, екеуі де «нәтижесі белгісіз» дегенді білдіреді:
| Код | HTTP | Мағынасы |
|---|---|---|
refund_unknown | 502 | Kaspi жауап бермеді, қайтару өтті ме, өтпеді ме — белгісіз |
refund_pending_unknown | 409 | Алдыңғы қайтарудың нәтижесі әлі белгісіз |
Осы екеуін көрсеңіз реті мынандай:
- Қайтаруды қайталамаңыз.
GET /api/v1/invoices/{id}арқылы счёттың күйін оқыңыз — жауапта қайтарулар тізімі де келеді.- Күйі
refundedнемесеpartially_refundedболса, қайтару өткен. Ештеңе істемеңіз. - Күйі әлі
paidболса, біраз күтіп қайта оқыңыз. - Жағдай ұзақ анықталмаса, қолдауға жазыңыз: WhatsApp +7 778 881 3333, Telegram @qutpaybot.
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 және Екі рет қайтарып жіберуден қалай сақтану.
Счёттар қосарланып кетсе
Егер қосарланған счёттар шығып жатса, бұл әдетте кодтағы цикл немесе идемпоттылықтың жоқтығы. Шұғыл қадамдар: Счёттар қосарланып жатыр.
Қосарлану тарифтің айлық лимитін де тез жеп қояды: лимит жасалған счёт бойынша есептеледі, төленгені бойынша емес.
Жиі қойылатын сұрақтар
Кілттің жарамдылық мерзімі бар ма? Кілт шексіз сақталмайды. Бір тапсырысты бірнеше ай өткен соң қайта жіберсеңіз, жаңа счёт жасалуы мүмкін — сондықтан тапсырыстың күйін өз базаңыздан да тексеріңіз.
Сол кілтпен басқа сомамен жіберсем ше? Кілт бұрынғы счётты қайтарады. Сома шынымен өзгерсе, жаңа кілт қолданыңыз: мысалы order-1001-pay-2.
Sandbox-та да жұмыс істей ме? Иә, бірдей.
Топтап счёт жасағанда ше? POST /api/v1/invoices/bulk ішіндегі әр элемент жеке тексеріледі. Тізімге әр тапсырыс бір рет кіретініне өз жағыңызда көз жеткізіңіз: Топтап счёт жасау.
Жазылымда идемпоттылық керек пе? Жазылым счёттарды өзі кестемен шығарады, ол жақ біздің жағымызда реттелген. Сізге тек webhook өңдеуін идемпотентті жазу керек.