# Создание счёта: все поля

> Полный справочник по POST /api/v1/invoices — тип и ограничение каждого поля, все поля ответа, примеры на curl и Node, разница между qr и phone и список частых ошибок с решениями.

## Коротко

Создание счёта — это один запрос:

```
POST https://api.qut.kz/api/v1/invoices
X-API-Key: qp_live_…
Content-Type: application/json
```

В ответе приходит статус 201 и `payUrl` — отправьте покупателя по этому адресу. Обязательное поле всего одно: `amount`.

Старый путь `POST /api/v1/orders` тоже принимается, это алиас того же эндпоинта.

## Поля запроса

| Поле | Тип | Обязательно | Описание |
|---|---|---|---|
| `amount` | number | ✅ | Тенге. В QR-счёте не более 2 знаков после запятой, в счёте по телефону — целое |
| `kind` | `qr` \| `phone` | — | `qr` — по умолчанию: QR-код и ссылка. `phone` — push в приложение Kaspi покупателя |
| `description` | string | — | Видит покупатель. QR — 100 символов, phone — 60 |
| `externalOrderId` | string | — | Ваш номер заказа. Возвращается в вебхуке |
| `customer.name` | string | — | Имя покупателя |
| `customer.phone` | string | ✅ для `kind: phone` | В формате `7XXXXXXXXXX`, 11 цифр |
| `customer.email` | string | — | Если указан, покупателю может уйти письмо с чеком |
| `successUrl` | url | — | Куда вернуть покупателя после успешной оплаты. Только http(s) |
| `failUrl` | url | — | Куда вернуть, если оплата не прошла. Только http(s) |
| `metadata` | object | — | Любой JSON. Хранится без изменений и возвращается в вебхуке |

Заголовки:

| Заголовок | Обязателен | Зачем |
|---|---|---|
| `X-API-Key` | ✅ | `qp_live_…` или `qp_test_…` |
| `Content-Type: application/json` | ✅ | Тело в JSON |
| `Idempotency-Key` | — | Защита от дублей, см. ниже |

Если передать `Idempotency-Key`, повторный запрос с тем же ключом не создаст новый счёт: вернётся прежний, с HTTP 200 и полем `idempotentReplay: true`. Подробнее: [Идемпотентность](/kb/ru/idempotency).

## Разница между qr и phone

| | `qr` | `phone` |
|---|---|---|
| Что видит покупатель | QR-код или ссылку на оплату | Push-счёт в приложении Kaspi |
| `customer.phone` | Не обязателен | Обязателен |
| Сумма | До 2 знаков после запятой | Только целые тенге |
| `description` | 100 символов | 60 символов |
| Когда удобно | Сайт, офлайн-точка, экран | Продажа по телефону, удалённо |

Полное сравнение: [QR-счёт или счёт по телефону](/kb/ru/qr-vs-phone).

## Ответ

HTTP 201 и тело такого вида:

| Поле | Что это |
|---|---|
| `id` | Идентификатор счёта, `inv_…` |
| `status` | При создании `pending` |
| `payUrl` | Страница оплаты, сюда отправляйте покупателя |
| `qrUrl` | Содержимое QR-кода |
| `deepLink` | Ссылка, открывающая приложение Kaspi |
| `qrImageUrl` | Картинка QR, можно вывести на своей странице |
| `expiresAt` | После этого времени счёт недействителен |

Окно сканирования QR — примерно три минуты, его задаёт Kaspi. Не зашивайте это число в код: всегда берите `expiresAt` из ответа.

## Пример на curl

```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' \
  -d '{
    "amount": 2500,
    "kind": "qr",
    "description": "Заказ №1001",
    "externalOrderId": "1001",
    "customer": { "name": "Айгуль", "phone": "77010000000" },
    "successUrl": "https://site.kz/ok",
    "failUrl": "https://site.kz/fail",
    "metadata": { "branch": "almaty-1", "cart": 17 }
  }'
```

## Пример на Node

```js
const res = 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,
    kind: 'qr',
    description: `Заказ №${order.id}`,
    externalOrderId: String(order.id),
    successUrl: 'https://site.kz/ok',
    metadata: { orderId: order.id },
  }),
});

if (!res.ok) {
  const err = await res.json();
  console.error('qutpay', res.status, err.error, err.message);
  throw new Error(err.error);
}

const invoice = await res.json();
redirect(invoice.payUrl);
```

При ошибке смотрите на код в поле `error`, а не на текст `message`: текст может измениться, код — нет.

## Алиас /orders

Для старых интеграций сохранён путь `POST /api/v1/orders`. Там дополнительно принимаются поля `merchantRef` (то же, что `externalOrderId`) и `method: "invoice"`. Если пишете новый код, используйте `/api/v1/invoices`.

## Частые ошибки

| Код | HTTP | Что случилось | Решение |
|---|---|---|---|
| `invalid_amount` | 422 | Суммы нет или это не число | Передайте положительное число |
| `amount_must_be_whole_tenge` | 422 | Переданы тиыны | Передайте целые тенге |
| `amount_too_small` / `amount_too_large` | 422 | Сумма вне допустимого диапазона | Исправьте сумму |
| `invalid_phone` | 422 | Неверный формат телефона | `7XXXXXXXXXX`, 11 цифр, без `+` и пробелов |
| `phone_required` | 422 | `kind: phone`, но телефона нет | Добавьте `customer.phone` |
| `invalid_kind` | 422 | Неизвестный тип | `qr` или `phone` |
| `invalid_url` | 422 | Неверный `successUrl`/`failUrl` | Укажите полный http(s)-адрес |
| `unauthorized` | 401 | Ключ не передан или недействителен | Проверьте заголовок `X-API-Key` |
| `insufficient_scope` | 403 | У ключа нет `invoices:write` | Добавьте право в кабинете |
| `kaspi_session_expired` | 409 | Привязка кассира оборвалась | Переподключите кассира |
| `tariff_limit_reached` | 429 | Закончился месячный лимит | [Тарифы и лимиты](/kb/ru/tariff-limits) |
| `invoice_create_failed` | 502 | Kaspi не принял счёт | Повторите с растущей паузой |

Полный список: [Каталог ошибок](/kb/ru/error-catalog).

## Пограничные случаи

- **Тиыны.** QR-счёт принимает два знака после запятой, счёт по телефону — только целые тенге. Если в сумме есть тиыны, округлите до отправки с `kind: phone`.
- **Длинное описание.** Текст сверх лимита у покупателя может обрезаться, поэтому не превышайте 100 символов для QR и 60 для phone.
- **Несколько счетов на один заказ.** Двойное нажатие кнопки «оплатить» создаст два счёта. Ставьте `Idempotency-Key`.
- **Нужно много счетов сразу.** До 100 счетов можно создать одним запросом: [Массовое создание счетов](/kb/ru/bulk-invoices).
- **Не дождались ответа.** При таймауте счёт мог быть создан. Повторите с тем же `Idempotency-Key` — новый не появится.

## Вопросы и ответы

**Чем `payUrl` отличается от `deepLink`?** `payUrl` — страница оплаты в браузере, работает на любом устройстве. `deepLink` открывает приложение Kaspi напрямую, удобен на телефоне.

**Как узнать статус счёта?** Через вебхук или запросом `GET /api/v1/invoices/{id}`. Что когда: [Вебхук или опрос статуса](/kb/ru/polling-vs-webhook).

**Что можно класть в `metadata`?** Любой JSON: номер точки, идентификатор корзины, номер ячейки. Он возвращается и в вебхуке, и в ответе на запрос статуса.

**Можно изменить сумму уже созданного счёта?** Нет. Отмените счёт и создайте новый.

**В песочнице те же поля?** Да, полностью. Разница только в том, что Kaspi не вызывается и оплату вы симулируете сами.
