# Топтап счёт жасау — бір сұрауда 100 счётқа дейін

> POST /api/v1/invoices/bulk әдісі: бір сұрауда 1-100 счёт, әр элемент бөлек тексеріледі, жауап құрылымы, қателерді элемент бойынша өңдеу, идемпоттылық және тариф лимитіне әсері.

## Қысқаша

Бір сұрауда бірнеше счёт жасау керек болса — `POST /api/v1/invoices/bulk`. Бір сұрауға **1-ден 100-ге дейін** элемент сыяды.

Ең маңызды қасиеті: **әр элемент бөлек тексеріледі және бөлек орындалады**. Тізімдегі біреуінің телефоны қате болса, қалғандарының бәрі бәрібір жасалады. Сондықтан жауапты «өтті / өтпеді» деп емес, әр элемент бойынша оқисыз.

## Сұрау

```http
POST /api/v1/invoices/bulk
X-API-Key: qp_live_…
Idempotency-Key: payroll-2026-09-14
Content-Type: application/json

{
  "invoices": [
    { "amount": 12000, "kind": "phone", "customer": { "phone": "77011234567" }, "externalOrderId": "drv-101" },
    { "amount": 12000, "kind": "phone", "customer": { "phone": "77017654321" }, "externalOrderId": "drv-102" },
    { "amount": 8500,  "kind": "qr",    "description": "Қыркүйек айы",        "externalOrderId": "drv-103" }
  ]
}
```

Әр элементтің ішінде — жалғыз счёт жасағандағы **сол өрістер**: `amount`, `kind`, `description`, `externalOrderId`, `customer`, `successUrl`, `failUrl`, `metadata`. Ешқандай жаңа өріс жоқ, ешқайсысы жоғалмайды.

| Шектеу | Мәні |
|---|---|
| Ең аз элемент | 1 |
| Ең көп элемент | 100 |
| Аралас түрлер | Бір тізімде `qr` да, `phone` да болады |
| Кілт | Бір кілт, бір ұйым, бір кассир |

## Жауап

Жауап коды — **207**: «әрқайсысында өз нәтижесі бар». Құрылымы:

```json
{
  "total": 3,
  "created": 2,
  "failed": 1,
  "results": [
    { "index": 0, "ok": true,  "id": "inv_7Kd2", "status": "pending", "payUrl": "https://qut.kz/p/…", "externalOrderId": "drv-101" },
    { "index": 1, "ok": true,  "id": "inv_7Kd3", "status": "pending", "payUrl": "https://qut.kz/p/…", "externalOrderId": "drv-102" },
    { "index": 2, "ok": false, "error": "invalid_amount", "message": "Сома дұрыс емес", "externalOrderId": "drv-103" }
  ]
}
```

| Өріс | Мағынасы |
|---|---|
| `total` | Жіберілген элемент саны |
| `created` | Сәтті жасалғаны |
| `failed` | Құлағаны |
| `results[].index` | Сіздің тізіміңіздегі реттік нөмір, 0-ден басталады |
| `results[].ok` | Осы элемент өтті ме |
| `results[].error` | Құласа — қате коды |

`results` массивінің реті сіз жіберген реттен өзгермейді, сондықтан `index` бойынша өз деректеріңізбен салыстыра аласыз. Бірақ сенімді болу үшін әр элементке `externalOrderId` жазып қойған дұрыс — ол жауапта да, кейін webhook-та да қайта келеді.

## Қателерді өңдеу

Негізгі қағида: **бүкіл топты емес, тек құлаған элементтерді қайта жіберіңіз**.

```js
const res = await fetch(`${API}/invoices/bulk`, {
  method: 'POST',
  headers: {
    'X-API-Key': KEY,
    'Content-Type': 'application/json',
    'Idempotency-Key': batchKey,
  },
  body: JSON.stringify({ invoices: batch }),
}).then((r) => r.json());

const retry = [];
for (const r of res.results) {
  if (r.ok) {
    saveInvoice(batch[r.index], r.id, r.payUrl);
    continue;
  }
  if (['invalid_amount', 'invalid_phone', 'amount_must_be_whole_tenge'].includes(r.error)) {
    // деректің өзі қате — қайталаудың мәні жоқ, операторға көрсетіңіз
    reportToOperator(batch[r.index], r.error);
  } else if (['invoice_create_failed', 'kaspi_error'].includes(r.error)) {
    // уақытша ақау — кейін қайталауға болады
    retry.push(batch[r.index]);
  }
}
```

Қате түрлерін былай бөліңіз:

| Қате тобы | Мысал кодтар | Не істеу керек |
|---|---|---|
| Деректің өзі қате | `invalid_amount`, `invalid_phone`, `invalid_kind`, `amount_must_be_whole_tenge` | Түзетпей қайталамаңыз |
| Уақытша ақау | `invoice_create_failed`, `kaspi_error` | Кідіріспен қайталаңыз |
| Лимит | `tariff_limit_reached`, `tariff_daily_burst` | Тоқтатыңыз, тарифті қараңыз |
| Байланыс | `kaspi_session_expired`, `no_provider` | Кассирді қалпына келтіріңіз, содан кейін қайталаңыз |

Бүкіл сұрау бірден құлайтын жағдайлар да бар: кілт жарамсыз (`unauthorized`), құқық жоқ (`insufficient_scope`), тізім бос немесе 100-ден асып кеткен. Ондайда `results` мүлдем келмейді, әдеттегі `{ error, message }` қайтады.

## Идемпоттылық

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

Бұл желі үзілген жағдайда өте маңызды: жауапты ала алмасаңыз, жаңа счёттар жасалып қалды ма деп қорықпай, сол кілтпен қайта жіберіп, нәтижені оқисыз.

Кілтті мазмұнға байланысты етіп жасаңыз, мысалы `payroll-2026-09-14` немесе `orders-batch-4471`. Ал әр элементтің ішіндегі `externalOrderId` жеке счёт деңгейінде қорғайды — екеуін қатар қолданған дұрыс. Толығырақ: [Счёттар қосарланып жатыр](/kb/duplicate-invoices).

## Тариф лимитіне әсері

Топтама — лимит үшін жеңілдік емес. **Сәтті жасалған әр счёт** айлық лимитке де, тәуліктік қорғанысқа да жеке-жеке есептеледі.

| Тариф | Айына счёт | Тәуліктік қорғаныс |
|---|---|---|
| Сынақ | — | 50 |
| Бастау | 800 | 200 |
| Бизнес | 4 000 | 1 500 |
| Про | 15 000 | 5 000 |

Яғни, Бастау тарифінде бір тәулікте 200-ден көп счёт жасай алмайсыз — 100-дік екі топтама лимитті толтырады. Лимитке жеткенде элементтер `tariff_limit_reached` немесе `tariff_daily_burst` қатесімен құлай бастайды. Екеуі екі басқа нәрсе: біріншісі — тарифіңіздің айлық лимиті, екіншісі — циклге түскен интеграциядан қорғаныс.

Sandbox счёттары лимитке кірмейді, сондықтан топтаманы алдымен `qp_test_…` кілтімен сынап көріңіз.

## Kaspi жиілік шектеуі болса

Kaspi өз жағынан сұрау жиілігін шектеуі мүмкін. Топтама жіберіп, элементтердің біраз бөлігі `invoice_create_failed` немесе `kaspi_error` қатесімен құлап жатса, бұл — жиілік белгісі.

Не істеу керек:

1. **Топтама көлемін кішірейтіңіз.** 100 орнына 20-25 элементтен жіберіңіз.
2. **Топтамалар арасына пауза қойыңыз.** 2-5 секунд жеткілікті.
3. **Өсіп отыратын кідіріспен қайталаңыз.** 1, 2, 4, 8 секунд.
4. `429` және `Retry-After` тақырыбы келсе, сол көрсетілген уақытты күтіңіз.
5. Барлық элемент бірдей құлап жатса, қайта-қайта жібермей **тоқтаңыз** — себебі көбіне кассир байланысында немесе тарифте.

Жаппай құлаудың себебін іздеу реті бөлек жазылған: [Счёттар жаппай құлап жатыр](/kb/mass-invoice-failures).

## Қашан топтама керек, қашан керек емес

| Сценарий | Дұрысы |
|---|---|
| Таксопарк жүргізушілерінен айлық жарна | Топтама |
| Курс тыңдаушыларына кезекті төлем | Топтама |
| Көтерме клиенттерге ай сайынғы есеп-шот | Топтама |
| Сайттағы бір тапсырыс | Жалғыз `POST /invoices` |
| Әрқайсысы әртүрлі уақытта шығатын кезеңдік төлем | Жазылым |

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

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

**Бір элемент құласа, қалғандары күшін жоя ма?** Жоқ. Жасалғаны жасалған күйінде қалады. Транзакция жоқ — топтама «бәрі немесе ештеңе» принципімен жұмыс істемейді.

**Webhook топтамаға бір рет келе ме?** Жоқ, әр счёт бойынша әдеттегі оқиғалар жеке келеді. Одан бөлек `invoice.bulk` оқиғасы топтаманың өзі туралы хабарлайды.

**100-ден көп жіберсем не болады?** Сұрау түгел қабылданбайды. Тізімді өзіңіз 100-ден кіші бөліктерге бөліңіз.

**Бір топтамада әртүрлі кассир бола ма?** Жоқ. Счёттар кілтке байланған кассир арқылы жасалады. Әр кассир үшін бөлек кілт қолданыңыз: [API кілттер](/kb/api-keys).

**Кабинеттен топтап счёт жасауға бола ма?** API арқылы жасалған счёттар кабинетте әдеттегідей көрінеді, экспортқа да түседі.
